为什么这个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,在这种情况下它更有意义并且更整洁。

如果我误解了这个解决方案,我不会感到惊讶,但它似乎有效,所以我会考虑解决这个问题。