为什么这个x86 shellcode运行成功了,然后触发了违规执行位置?
Why does this x86 shellcode run successfully but then trigger a violation executing location?
问题:为什么shellcode成功完成后,程序会抛出一个violation executing location异常?
描述: 我的 objective 是使用调用 Windows API 函数的 x86 shellcode 将 DLL 加载和卸载到当前程序中.当程序成功完成此目标时,Visual Studio 会告诉我执行某个位置时存在违规行为。我知道程序执行成功,因为测试 DLL 文件在附加和分离时打印。另一个需要注意的重要细节是,这只发生在调用卸载函数时,加载函数绝对没有问题。 (如果这很重要,我将在 Visual Studio 2019 年 Windows 10 日使用 C++20 执行此操作)
我知道 shellcode 没有正确设置堆栈帧,但我确保在 return 执行被调用函数之前将 ESP 设置回正常状态。我保存了 EAX 并在卸载函数中也将其设置为正常。我制作这个测试程序的最终目标是生成 shellcode,我可以将其用于我正在处理的 dll 注入程序中的远程线程上下文修补方法。我还多次验证了用于查找 return 地址的偏移量。感谢任何帮助,谢谢!
这是控制台输出。
Attached! DLLMain at 0x79EF134D
Detached!
这是抛出的异常。
Exception thrown at 0x9269D814 in Shellcode DLL
Loading.exe: 0xC0000005: Access violation executing
location 0x9269D814.
这是主要文件,只有 120 行左右。
const dword follow_relative_jump(const pbyte pointer)
{
if (pointer)
{
if (pointer[0] == 0xE9 || pointer[0] == 0xEB)
{
return reinterpret_cast<dword>(pointer + 5 + reinterpret_cast<psdword>(pointer + 1)[0]);
}
}
return reinterpret_cast<dword>(pointer);
}
void load_dll(const dword path_address)
{
/*
68 90 90 90 90 -> push 0x???????? (return address buffer)
68 90 90 90 90 -> push 0x???????? (LoadLibraryA() address buffer)
68 90 90 90 90 -> push 0x???????? (DLL path address buffer)
FF 54 24 04 -> call [esp + 4] (calling LoadLibraryA())
83 C4 08 -> add esp, 8 (cleaning up the stack, except for return address)
C3 -> ret (return to return address that was pushed first, it should pop it off the stack and return ESP to normal)
*/
std::vector<byte> shellcode = {
0x68, 0x90, 0x90, 0x90, 0x90,
0x68, 0x90, 0x90, 0x90, 0x90,
0x68, 0x90, 0x90, 0x90, 0x90,
0xFF, 0x54, 0x24, 0x04,
0x83, 0xC4, 0x08,
0xC3
};
// Offset is the distance from the function prologue to the next instruction after the call to load_dll()
reinterpret_cast<pdword>(shellcode.data() + 1)[0] = follow_relative_jump(reinterpret_cast<pbyte>(&load_dll)) + 0x22C;
reinterpret_cast<pdword>(shellcode.data() + 6)[0] = reinterpret_cast<dword>(&LoadLibraryA);
reinterpret_cast<pdword>(shellcode.data() + 11)[0] = path_address;
if (const auto allocation = VirtualAlloc(NULL, shellcode.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE))
{
memcpy(allocation, shellcode.data(), shellcode.size());
reinterpret_cast<void(__cdecl*)()>(allocation)();
VirtualFree(allocation, shellcode.size(), MEM_FREE);
}
}
void unload_dll(const dword path_address)
{
/*
68 90 90 90 90 -> push 0x???????? (return address buffer)
50 -> push eax (save EAX so we can set it back later)
68 90 90 90 90 -> push 0x???????? (GetModuleHandleA() address buffer)
68 90 90 90 90 -> push 0x???????? (DLL path address buffer)
FF 54 24 04 -> call [esp + 4] (calling GetModuleHandleA())
83 C4 08 -> add esp, 8 (clean up the stack, except for return address and saved EAX)
68 90 90 90 90 -> push 0x???????? (FreeLibrary() address buffer)
50 -> push eax (Handle to module returned by GetModuleHandleA() in EAX)
FF 54 24 04 -> call [esp + 4] (calling FreeLibrary())
83 C4 08 -> add esp, 8 (clean up stack, except for return address and saved EAX)
58 -> pop eax (set back EAX to what it was before)
C3 -> ret (return to return address that was pushed first, it should pop it off the stack and return ESP to normal)
*/
std::vector<byte> shellcode = {
0x68, 0x90, 0x90, 0x90, 0x90,
0x50,
0x68, 0x90, 0x90, 0x90, 0x90,
0x68, 0x90, 0x90, 0x90, 0x90,
0xFF, 0x54, 0x24, 0x04,
0x83, 0xC4, 0x08,
0x68, 0x90, 0x90, 0x90, 0x90,
0x50,
0xFF, 0x54, 0x24, 0x04,
0x83, 0xC4, 0x08,
0x58,
0xC3
};
// Offset is the distance from the function prologue to the next instruction after the call to unload_dll()
reinterpret_cast<pdword>(shellcode.data() + 1)[0] = follow_relative_jump(reinterpret_cast<pbyte>(&unload_dll)) + 0x2AF;
reinterpret_cast<pdword>(shellcode.data() + 7)[0] = reinterpret_cast<dword>(&GetModuleHandleA);
reinterpret_cast<pdword>(shellcode.data() + 12)[0] = path_address;
reinterpret_cast<pdword>(shellcode.data() + 24)[0] = reinterpret_cast<dword>(&FreeLibrary);
if (const auto allocation = VirtualAlloc(NULL, shellcode.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE))
{
memcpy(allocation, shellcode.data(), shellcode.size());
reinterpret_cast<void(__cdecl*)()>(allocation)();
VirtualFree(allocation, shellcode.size(), MEM_FREE);
}
}
int main()
{
const char* path = "C:\Users\maxbd\Desktop\test.dll";
load_dll(reinterpret_cast<dword>(path));
unload_dll(reinterpret_cast<dword>(path));
static_cast<void>(std::getchar());
return 0;
}
我没有考虑我尝试调用的函数的调用约定以及它们的预期运行方式。 Windows API 函数使用 __stdcall
将函数参数从函数中的堆栈弹出。所以我应该只弹出我推送的函数地址而不是函数参数。小丑,感谢您在评论中提供的信息。
此外,我必须将 0xC3
return 指令更改为 0xC2 0x04 0x00
以便它会将 return 地址从堆栈中弹出。我以为正常的 0xC3
return 会为我做那件事,但显然它没有。或者至少出于某种原因在这种情况下不是这样。 Visual Studio 如果我不手动将其弹出,则会抛出有关 ESP 不正确的异常。如果我这样做,它在加载和卸载 DLL 时都能完美运行。
我也完全忘记了,因为这是一个测试程序,所以我使用函数指针将 shellcode 作为 __cdecl
函数调用,而不是劫持远程线程的执行并修改 EIP,所以 [=正在使用 15=] 指令,所以我没有理由手动推送 return 地址。 我假设我自愿未能正确设置堆栈帧是 return 行为异常的原因,因为 return 地址应该在 EBP 之上。 因为使用了call
,return地址被压入了两次,所以需要在returning之后弹出一个dword的return指令来摆脱自动压入return 地址。当我将 shellcode 应用到我的实际程序时,我将尝试相对跳转而不是 return,在这种情况下它更有意义并且更整洁。
如果我误解了这个解决方案,我不会感到惊讶,但它似乎有效,所以我会考虑解决这个问题。
问题:为什么shellcode成功完成后,程序会抛出一个violation executing location异常?
描述: 我的 objective 是使用调用 Windows API 函数的 x86 shellcode 将 DLL 加载和卸载到当前程序中.当程序成功完成此目标时,Visual Studio 会告诉我执行某个位置时存在违规行为。我知道程序执行成功,因为测试 DLL 文件在附加和分离时打印。另一个需要注意的重要细节是,这只发生在调用卸载函数时,加载函数绝对没有问题。 (如果这很重要,我将在 Visual Studio 2019 年 Windows 10 日使用 C++20 执行此操作)
我知道 shellcode 没有正确设置堆栈帧,但我确保在 return 执行被调用函数之前将 ESP 设置回正常状态。我保存了 EAX 并在卸载函数中也将其设置为正常。我制作这个测试程序的最终目标是生成 shellcode,我可以将其用于我正在处理的 dll 注入程序中的远程线程上下文修补方法。我还多次验证了用于查找 return 地址的偏移量。感谢任何帮助,谢谢!
这是控制台输出。
Attached! DLLMain at 0x79EF134D
Detached!
这是抛出的异常。
Exception thrown at 0x9269D814 in Shellcode DLL
Loading.exe: 0xC0000005: Access violation executing
location 0x9269D814.
这是主要文件,只有 120 行左右。
const dword follow_relative_jump(const pbyte pointer)
{
if (pointer)
{
if (pointer[0] == 0xE9 || pointer[0] == 0xEB)
{
return reinterpret_cast<dword>(pointer + 5 + reinterpret_cast<psdword>(pointer + 1)[0]);
}
}
return reinterpret_cast<dword>(pointer);
}
void load_dll(const dword path_address)
{
/*
68 90 90 90 90 -> push 0x???????? (return address buffer)
68 90 90 90 90 -> push 0x???????? (LoadLibraryA() address buffer)
68 90 90 90 90 -> push 0x???????? (DLL path address buffer)
FF 54 24 04 -> call [esp + 4] (calling LoadLibraryA())
83 C4 08 -> add esp, 8 (cleaning up the stack, except for return address)
C3 -> ret (return to return address that was pushed first, it should pop it off the stack and return ESP to normal)
*/
std::vector<byte> shellcode = {
0x68, 0x90, 0x90, 0x90, 0x90,
0x68, 0x90, 0x90, 0x90, 0x90,
0x68, 0x90, 0x90, 0x90, 0x90,
0xFF, 0x54, 0x24, 0x04,
0x83, 0xC4, 0x08,
0xC3
};
// Offset is the distance from the function prologue to the next instruction after the call to load_dll()
reinterpret_cast<pdword>(shellcode.data() + 1)[0] = follow_relative_jump(reinterpret_cast<pbyte>(&load_dll)) + 0x22C;
reinterpret_cast<pdword>(shellcode.data() + 6)[0] = reinterpret_cast<dword>(&LoadLibraryA);
reinterpret_cast<pdword>(shellcode.data() + 11)[0] = path_address;
if (const auto allocation = VirtualAlloc(NULL, shellcode.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE))
{
memcpy(allocation, shellcode.data(), shellcode.size());
reinterpret_cast<void(__cdecl*)()>(allocation)();
VirtualFree(allocation, shellcode.size(), MEM_FREE);
}
}
void unload_dll(const dword path_address)
{
/*
68 90 90 90 90 -> push 0x???????? (return address buffer)
50 -> push eax (save EAX so we can set it back later)
68 90 90 90 90 -> push 0x???????? (GetModuleHandleA() address buffer)
68 90 90 90 90 -> push 0x???????? (DLL path address buffer)
FF 54 24 04 -> call [esp + 4] (calling GetModuleHandleA())
83 C4 08 -> add esp, 8 (clean up the stack, except for return address and saved EAX)
68 90 90 90 90 -> push 0x???????? (FreeLibrary() address buffer)
50 -> push eax (Handle to module returned by GetModuleHandleA() in EAX)
FF 54 24 04 -> call [esp + 4] (calling FreeLibrary())
83 C4 08 -> add esp, 8 (clean up stack, except for return address and saved EAX)
58 -> pop eax (set back EAX to what it was before)
C3 -> ret (return to return address that was pushed first, it should pop it off the stack and return ESP to normal)
*/
std::vector<byte> shellcode = {
0x68, 0x90, 0x90, 0x90, 0x90,
0x50,
0x68, 0x90, 0x90, 0x90, 0x90,
0x68, 0x90, 0x90, 0x90, 0x90,
0xFF, 0x54, 0x24, 0x04,
0x83, 0xC4, 0x08,
0x68, 0x90, 0x90, 0x90, 0x90,
0x50,
0xFF, 0x54, 0x24, 0x04,
0x83, 0xC4, 0x08,
0x58,
0xC3
};
// Offset is the distance from the function prologue to the next instruction after the call to unload_dll()
reinterpret_cast<pdword>(shellcode.data() + 1)[0] = follow_relative_jump(reinterpret_cast<pbyte>(&unload_dll)) + 0x2AF;
reinterpret_cast<pdword>(shellcode.data() + 7)[0] = reinterpret_cast<dword>(&GetModuleHandleA);
reinterpret_cast<pdword>(shellcode.data() + 12)[0] = path_address;
reinterpret_cast<pdword>(shellcode.data() + 24)[0] = reinterpret_cast<dword>(&FreeLibrary);
if (const auto allocation = VirtualAlloc(NULL, shellcode.size(), MEM_COMMIT | MEM_RESERVE, PAGE_EXECUTE_READWRITE))
{
memcpy(allocation, shellcode.data(), shellcode.size());
reinterpret_cast<void(__cdecl*)()>(allocation)();
VirtualFree(allocation, shellcode.size(), MEM_FREE);
}
}
int main()
{
const char* path = "C:\Users\maxbd\Desktop\test.dll";
load_dll(reinterpret_cast<dword>(path));
unload_dll(reinterpret_cast<dword>(path));
static_cast<void>(std::getchar());
return 0;
}
我没有考虑我尝试调用的函数的调用约定以及它们的预期运行方式。 Windows API 函数使用 __stdcall
将函数参数从函数中的堆栈弹出。所以我应该只弹出我推送的函数地址而不是函数参数。小丑,感谢您在评论中提供的信息。
此外,我必须将 0xC3
return 指令更改为 0xC2 0x04 0x00
以便它会将 return 地址从堆栈中弹出。我以为正常的 0xC3
return 会为我做那件事,但显然它没有。或者至少出于某种原因在这种情况下不是这样。 Visual Studio 如果我不手动将其弹出,则会抛出有关 ESP 不正确的异常。如果我这样做,它在加载和卸载 DLL 时都能完美运行。
我也完全忘记了,因为这是一个测试程序,所以我使用函数指针将 shellcode 作为 __cdecl
函数调用,而不是劫持远程线程的执行并修改 EIP,所以 [=正在使用 15=] 指令,所以我没有理由手动推送 return 地址。 我假设我自愿未能正确设置堆栈帧是 return 行为异常的原因,因为 return 地址应该在 EBP 之上。 因为使用了call
,return地址被压入了两次,所以需要在returning之后弹出一个dword的return指令来摆脱自动压入return 地址。当我将 shellcode 应用到我的实际程序时,我将尝试相对跳转而不是 return,在这种情况下它更有意义并且更整洁。
如果我误解了这个解决方案,我不会感到惊讶,但它似乎有效,所以我会考虑解决这个问题。