0

问题:为什么程序在shellcode成功完成它的目标后会抛出一个违反执行位置异常?

描述:我的目标是使用调用 Windows API 函数的 x86 shellcode 将 DLL 加载和卸载到当前程序中。虽然程序成功地完成了这个目标,但 Visual Studio 然后告诉我执行位置存在违规行为。我知道程序成功执行,因为测试 DLL 文件在附加和分离时打印。另一个需要注意的重要细节是这仅在调用卸载函数时发生,加载函数绝对没有问题。(如果这很重要,我正在使用 C++20 在 Visual Studio 2019 的 Windows 10 上执行此操作)

我知道 shellcode 没有正确设置堆栈帧,但在将执行返回到被调用函数之前,我确保 ESP 已恢复正常。我保存了 EAX 并在卸载功能中将其恢复正常。我制作这个测试程序的最终目标是生成可用于我正在开发的 dll 注入程序中的远程线程上下文修补方法的 shellcode。我还多次验证了用于查找返回地址的偏移量。任何帮助表示赞赏,谢谢!

这是控制台输出。

随附的!DLLMain 位于 0x79EF134D
已分离!

这是抛出的异常。

在 Shellcode DLL
Loading.exe 中的 0x9269D814 处引发异常:0xC0000005:执行
位置 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;
}
4

1 回答 1

1

我没有考虑我试图调用的函数的调用约定以及它们应该如何操作。Windows API 函数使用__stdcall它从函数内的堆栈中弹出函数参数。所以我应该只弹出我推送的函数地址而不是函数参数。感谢您评论中的信息,小丑。

另外,我必须将0xC3返回指令更改为,0xC2 0x04 0x00以便它将返回地址从堆栈中弹出。我认为正常的0xC3回报会为我做到这一点,但显然事实并非如此。或者至少在这种情况下由于某种原因它不会。如果我不手动弹出它,Visual Studio 会抛出一个关于 ESP 不正确的异常。如果我这样做了,它在加载和卸载 DLL 时都能完美运行。

我也完全忘记了,因为这是一个测试程序,所以我__cdecl使用函数指针将 shellcode 作为函数调用,而不是劫持远程线程的执行和修改 EIP,所以call正在使用该指令,所以我没有理由推送手动返回地址。我假设我自愿未能正确设置堆栈帧是返回行为异常的原因,因为返回地址应该高于 EBP。因为call使用时,返回地址被压入两次,所以需要在返回后弹出一个dword的return指令来摆脱自动压入的返回地址。当我将 shellcode 应用到我的实际程序时,我会尝试相对跳转而不是返回,在这种情况下它更有意义并且更简洁。

如果我误解了这个解决方案,我不会感到惊讶,但它似乎有效,所以我会认为它已解决。

于 2021-09-04T03:11:48.693 回答