diff --git a/.gitignore b/.gitignore index 8084ca0..f31260c 100644 --- a/.gitignore +++ b/.gitignore @@ -31,7 +31,9 @@ *.out *.app +.vs src/wtf/fuzzer_* src/build/ src/build_msvc/ -targets/ \ No newline at end of file +src/out +targets/ diff --git a/src/hevd_client/hevd_client.cc b/src/hevd_client/hevd_client.cc index 2f69f63..1b7b6b0 100644 --- a/src/hevd_client/hevd_client.cc +++ b/src/hevd_client/hevd_client.cc @@ -4,7 +4,7 @@ #include #include -int main() { +int main(int argc, char *argv[]) { HANDLE H = CreateFileA(R"(\\.\GLOBALROOT\Device\HackSysExtremeVulnerableDriver)", GENERIC_ALL, 0, nullptr, OPEN_EXISTING, 0, nullptr); @@ -13,14 +13,73 @@ int main() { return EXIT_FAILURE; } - std::array Buffer; + std::array BufferBacking = {}; if (getenv("BREAK") != nullptr) { __debugbreak(); } DWORD Returned = 0; - DeviceIoControl(H, 0xdeadbeef, Buffer.data(), Buffer.size(), Buffer.data(), - Buffer.size(), &Returned, nullptr); + DWORD IoctlCode = 0xdeadbeef; + PVOID Buffer = BufferBacking.data(); + size_t BufferSize = BufferBacking.size(); + if (argc > 1) { + FILE *File = fopen(argv[1], "rb"); + if (File == nullptr) { + printf("fopen failed, bailing.\n"); + return EXIT_FAILURE; + } + + if (fseek(File, 0, SEEK_END) != 0) { + printf("fseek to the end failed, bailing.\n"); + fclose(File); + return EXIT_FAILURE; + } + + const long R = ftell(File); + if (R == -1 || R < 0) { + printf("ftell failed, bailing.\n"); + fclose(File); + return EXIT_FAILURE; + } + + const auto TotalSize = uint32_t(R); + if (fseek(File, 0, SEEK_SET) != 0) { + printf("fseek back to the beginning failed, bailing.\n"); + fclose(File); + return EXIT_FAILURE; + } + + if (fread(&IoctlCode, sizeof(IoctlCode), 1, File) != 1) { + printf("fread failed when reading ioctl code, bailing.\n"); + fclose(File); + return EXIT_FAILURE; + } + + BufferSize = TotalSize - sizeof(uint32_t); + Buffer = calloc(BufferSize, 1); + if (Buffer == nullptr) { + printf("calloc failed, bailing.\n"); + fclose(File); + return EXIT_FAILURE; + } + + if (fread(Buffer, BufferSize, 1, File) != 1) { + printf("fread failed when reading buffer, bailing.\n"); + fclose(File); + free(Buffer); + return EXIT_FAILURE; + } + + fclose(File); + } + + DeviceIoControl(H, IoctlCode, Buffer, BufferSize, Buffer, BufferSize, + &Returned, nullptr); CloseHandle(H); + + if (Buffer != BufferBacking.data()) { + free(Buffer); + } + return EXIT_SUCCESS; } \ No newline at end of file diff --git a/src/wtf/backend.cc b/src/wtf/backend.cc index 85f9489..e133db3 100644 --- a/src/wtf/backend.cc +++ b/src/wtf/backend.cc @@ -145,7 +145,8 @@ bool Backend_t::SimulateReturnFromFunction(const uint64_t Return) { return true; } -bool Backend_t::SimulateReturnFrom32bitFunction(const uint32_t Return, const uint32_t StdcallArgsCount) { +bool Backend_t::SimulateReturnFrom32bitFunction( + const uint32_t Return, const uint32_t StdcallArgsCount) { // // Set return value. // @@ -164,6 +165,16 @@ bool Backend_t::SimulateReturnFrom32bitFunction(const uint32_t Return, const uin return true; } +Gva_t Backend_t::GetArgAddress(const uint64_t Idx) { + if (Idx <= 3) { + fmt::print("The first four arguments are stored in registers (@rcx, @rdx, " + "@r8, @r9) which means you cannot get their addresses.\n"); + std::abort(); + } + + return Gva_t(Rsp() + (8 + (Idx * 8))); +} + uint64_t Backend_t::GetArg(const uint64_t Idx) { switch (Idx) { case 0: @@ -175,14 +186,21 @@ uint64_t Backend_t::GetArg(const uint64_t Idx) { case 3: return R9(); default: { - const Gva_t ArgPtr = Gva_t(Rsp() + (8 + (Idx * 8))); - return VirtRead8(ArgPtr); + return VirtRead8(GetArgAddress(Idx)); } } } Gva_t Backend_t::GetArgGva(const uint64_t Idx) { return Gva_t(GetArg(Idx)); } +std::pair Backend_t::GetArgAndAddress(const uint64_t Idx) { + return {GetArg(Idx), GetArgAddress(Idx)}; +} + +std::pair Backend_t::GetArgAndAddressGva(const uint64_t Idx) { + return {GetArgGva(Idx), GetArgAddress(Idx)}; +} + bool Backend_t::SaveCrash(const Gva_t ExceptionAddress, const uint32_t ExceptionCode) { const auto ExceptionCodeStr = ExceptionCodeToStr(ExceptionCode); diff --git a/src/wtf/backend.h b/src/wtf/backend.h index fe13d9f..db4376b 100644 --- a/src/wtf/backend.h +++ b/src/wtf/backend.h @@ -322,17 +322,17 @@ class Backend_t { // Read a uint64_t. // - uint64_t VirtRead8(const Gva_t Gva) const; - Gva_t VirtReadGva(const Gva_t Gva) const; - Gpa_t VirtReadGpa(const Gva_t Gva) const; + [[nodiscard]] uint64_t VirtRead8(const Gva_t Gva) const; + [[nodiscard]] Gva_t VirtReadGva(const Gva_t Gva) const; + [[nodiscard]] Gpa_t VirtReadGpa(const Gva_t Gva) const; // // Read a basic string. // template - std::basic_string<_Ty> VirtReadBasicString(const Gva_t StringGva, - const uint64_t MaxLength) const { + [[nodiscard]] std::basic_string<_Ty> + VirtReadBasicString(const Gva_t StringGva, const uint64_t MaxLength) const { using BasicString_t = std::basic_string<_Ty>; using ValueType_t = typename BasicString_t::value_type; @@ -432,15 +432,15 @@ class Backend_t { // Read a basic_string. // - std::string VirtReadString(const Gva_t Gva, - const uint64_t MaxLength = 256) const; + [[nodiscard]] std::string + VirtReadString(const Gva_t Gva, const uint64_t MaxLength = 256) const; // // Read a basic_string (used to read wchar_t* in Windows guests). // - std::u16string VirtReadWideString(const Gva_t Gva, - const uint64_t MaxLength = 256) const; + [[nodiscard]] std::u16string + VirtReadWideString(const Gva_t Gva, const uint64_t MaxLength = 256) const; // // Write in virtual memory. Optionally track dirtiness on the memory range. @@ -481,85 +481,98 @@ class Backend_t { // bool SimulateReturnFromFunction(const uint64_t Return); - bool SimulateReturnFrom32bitFunction(const uint32_t Return, const uint32_t StdcallArgsCount = 0); + bool SimulateReturnFrom32bitFunction(const uint32_t Return, + const uint32_t StdcallArgsCount = 0); // // Utility function that grabs function arguments according to the Windows x64 // calling convention. // - uint64_t GetArg(const uint64_t Idx); - Gva_t GetArgGva(const uint64_t Idx); + [[nodiscard]] uint64_t GetArg(const uint64_t Idx); + [[nodiscard]] Gva_t GetArgGva(const uint64_t Idx); + + // + // Utility function to get the address of a function argument. Oftentimes, you + // need to overwrite an argument that isn't stored in registers which means + // you need to calculate yourself where it is stored on the stack. This + // function gives you its address. + // + + [[nodiscard]] Gva_t GetArgAddress(const uint64_t Idx); + [[nodiscard]] std::pair GetArgAndAddress(const uint64_t Idx); + [[nodiscard]] std::pair GetArgAndAddressGva(const uint64_t Idx); + // // Shortcuts to grab / set some registers. // - uint64_t Rsp(); + [[nodiscard]] uint64_t Rsp(); void Rsp(const uint64_t Value); void Rsp(const Gva_t Value); - uint64_t Rbp(); + [[nodiscard]] uint64_t Rbp(); void Rbp(const uint64_t Value); void Rbp(const Gva_t Value); - uint64_t Rip(); + [[nodiscard]] uint64_t Rip(); void Rip(const uint64_t Value); void Rip(const Gva_t Value); - uint64_t Rax(); + [[nodiscard]] uint64_t Rax(); void Rax(const uint64_t Value); void Rax(const Gva_t Value); - uint64_t Rbx(); + [[nodiscard]] uint64_t Rbx(); void Rbx(const uint64_t Value); void Rbx(const Gva_t Value); - uint64_t Rcx(); + [[nodiscard]] uint64_t Rcx(); void Rcx(const uint64_t Value); void Rcx(const Gva_t Value); - uint64_t Rdx(); + [[nodiscard]] uint64_t Rdx(); void Rdx(const uint64_t Value); void Rdx(const Gva_t Value); - uint64_t Rsi(); + [[nodiscard]] uint64_t Rsi(); void Rsi(const uint64_t Value); void Rsi(const Gva_t Value); - uint64_t Rdi(); + [[nodiscard]] uint64_t Rdi(); void Rdi(const uint64_t Value); void Rdi(const Gva_t Value); - uint64_t R8(); + [[nodiscard]] uint64_t R8(); void R8(const uint64_t Value); void R8(const Gva_t Value); - uint64_t R9(); + [[nodiscard]] uint64_t R9(); void R9(const uint64_t Value); void R9(const Gva_t Value); - uint64_t R10(); + [[nodiscard]] uint64_t R10(); void R10(const uint64_t Value); void R10(const Gva_t Value); - uint64_t R11(); + [[nodiscard]] uint64_t R11(); void R11(const uint64_t Value); void R11(const Gva_t Value); - uint64_t R12(); + [[nodiscard]] uint64_t R12(); void R12(const uint64_t Value); void R12(const Gva_t Value); - uint64_t R13(); + [[nodiscard]] uint64_t R13(); void R13(const uint64_t Value); void R13(const Gva_t Value); - uint64_t R14(); + [[nodiscard]] uint64_t R14(); void R14(const uint64_t Value); void R14(const Gva_t Value); - uint64_t R15(); + [[nodiscard]] uint64_t R15(); void R15(const uint64_t Value); void R15(const Gva_t Value); diff --git a/src/wtf/fuzzer_hevd.cc b/src/wtf/fuzzer_hevd.cc index 44762e1..5b8de0c 100644 --- a/src/wtf/fuzzer_hevd.cc +++ b/src/wtf/fuzzer_hevd.cc @@ -49,8 +49,7 @@ bool InsertTestcase(const uint8_t *Buffer, const size_t BufferSize) { } g_Backend->R9(IoctlBufferSize); - const Gva_t Rsp = Gva_t(g_Backend->Rsp()); - const Gva_t OutBufferSizePtr = Rsp + Gva_t(5 * sizeof(uint64_t)); + const auto &OutBufferSizePtr = g_Backend->GetArgAddress(5); if (!g_Backend->VirtWriteStructDirty(OutBufferSizePtr, &IoctlBufferSize)) { DebugPrint("VirtWriteStructDirty failed\n"); return false; diff --git a/src/wtf/fuzzer_ioctl.cc b/src/wtf/fuzzer_ioctl.cc new file mode 100644 index 0000000..4d32fa9 --- /dev/null +++ b/src/wtf/fuzzer_ioctl.cc @@ -0,0 +1,252 @@ +// 1ndahous3 - March 4 2023 +#include "backend.h" +#include "targets.h" +#include + +// +// This fuzzing module expects a snapshot made at nt!NtDeviceIoControlFile. +// It is recommended to grab a snapshot with the biggest InputBufferLength +// possible. +// + +namespace Ioctl { + +constexpr bool DebugLoggingOn = false; +constexpr bool MutateIoctl = true; + +template +void DebugPrint(const char *Format, const Args_t &...args) { + if constexpr (DebugLoggingOn) { + fmt::print("Ioctl: "); + fmt::print(fmt::runtime(Format), args...); + } +} + +bool InsertTestcase(const uint8_t *Buffer, const size_t BufferSize) { + + // + // If we are mutating the IoControlCode, we expect at least 4 bytes. + // + + if constexpr (MutateIoctl) { + if (BufferSize < sizeof(uint32_t)) { + return true; + } + } + + // + // If we're mutating the IoControlCode, then the first 4 bytes are + // that. + // + + constexpr uint32_t IoctlSizeIfPresent = MutateIoctl ? sizeof(uint32_t) : 0; + + // + // We can only insert testcases that are smaller or equal to the + // current size; otherwise we'll corrupt memory. To work around + // this, we truncate it if it's larger. + // We also modify the InputBuffer pointer to push it as close as possible from + // the end of the buffer. + // + + // + // __kernel_entry NTSTATUS + // NtDeviceIoControlFile( + // [in] HANDLE FileHandle, + // [in] HANDLE Event, + // [in] PIO_APC_ROUTINE ApcRoutine, + // [in] PVOID ApcContext, + // [out] PIO_STATUS_BLOCK IoStatusBlock, + // [in] ULONG IoControlCode, + // [in] PVOID InputBuffer, + // [in] ULONG InputBufferLength, + // [out] PVOID OutputBuffer, + // [in] ULONG OutputBufferLength + // ); + // + + const uint32_t TotalInputBufferSize = BufferSize - IoctlSizeIfPresent; + const auto MutatedIoControlCodePtr = (uint32_t *)Buffer; + const uint8_t *MutatedInputBufferPtr = Buffer + IoctlSizeIfPresent; + + // + // Calculate the maximum size we can inject into the target. Either we can + // inject it all, or we need to truncate it. + // + + const auto &[InputBufferSize, InputBufferSizePtr] = + g_Backend->GetArgAndAddress(7); + const uint32_t MutatedInputBufferSize = + std::min(TotalInputBufferSize, uint32_t(InputBufferSize)); + + // + // Calculate the new InputBuffer address by pushing the mutated buffer as + // close as possible from its end. + // + + const auto &[InputBuffer, InputBufferPtr] = g_Backend->GetArgAndAddress(6); + const auto NewInputBuffer = + Gva_t(InputBuffer + InputBufferSize - MutatedInputBufferSize); + + // + // Fix up InputBufferLength. + // + + if (!g_Backend->VirtWriteStructDirty(InputBufferSizePtr, + &MutatedInputBufferSize)) { + fmt::print("Failed to fix up the InputBufferSize\n"); + std::abort(); + } + + // + // Fix up InputBuffer. + // + + if (!g_Backend->VirtWriteStructDirty(InputBufferPtr, &NewInputBuffer)) { + fmt::print("Failed to fix up the InputBuffer\n"); + std::abort(); + } + + // + // Insert the testcase at the new InputBuffer. + // + + if (!g_Backend->VirtWriteDirty(NewInputBuffer, MutatedInputBufferPtr, + MutatedInputBufferSize)) { + fmt::print("Failed to insert the testcase\n"); + std::abort(); + } + + // + // Are we mutating IoControlCode as well? + // + + if constexpr (MutateIoctl) { + const auto MutatedIoControlCode = *MutatedIoControlCodePtr; + const auto &IoControlCodePtr = g_Backend->GetArgAddress(5); + if (!g_Backend->VirtWriteStructDirty(IoControlCodePtr, + &MutatedIoControlCode)) { + fmt::print("Failed to VirtWriteStructDirty (Ioctl) failed\n"); + std::abort(); + } + } + + return true; +} + +bool Init(const Options_t &Opts, const CpuState_t &) { + + // + // Break on nt!NtDeviceIoControlFile. This is at that moment that we'll insert + // the testcase. + // + + if (!g_Backend->SetBreakpoint( + "nt!NtDeviceIoControlFile", [](Backend_t *Backend) { + // + // The first time we hit this breakpoint, we + // grab the return address and we set a + // breakpoint there to finish the testcase. + // + + static bool SetExitBreakpoint = false; + if (!SetExitBreakpoint) { + SetExitBreakpoint = true; + const auto ReturnAddress = + Backend->VirtReadGva(Gva_t(Backend->Rsp())); + if (!Backend->SetBreakpoint( + ReturnAddress, [](Backend_t *Backend) { + // + // Ok we're done! + // + + DebugPrint("Hit return breakpoint!\n"); + Backend->Stop(Ok_t()); + })) { + fmt::print("Failed to set breakpoint on return\n"); + std::abort(); + } + } + })) { + fmt::print("Failed to SetBreakpoint NtDeviceIoControlFile\n"); + return false; + } + + // + // NOP the calls to DbgPrintEx. + // + + if (!g_Backend->SetBreakpoint("nt!DbgPrintEx", [](Backend_t *Backend) { + const Gva_t FormatPtr = Backend->GetArgGva(2); + const std::string &Format = Backend->VirtReadString(FormatPtr); + DebugPrint("DbgPrintEx: {}", Format); + Backend->SimulateReturnFromFunction(0); + })) { + fmt::print("Failed to SetBreakpoint DbgPrintEx\n"); + return false; + } + + // + // Make ExGenRandom deterministic. + // + // kd> ub fffff805`3b8287c4 l1 + // nt!ExGenRandom+0xe0: + // fffff805`3b8287c0 480fc7f2 rdrand rdx + const Gva_t ExGenRandom = Gva_t(g_Dbg.GetSymbol("nt!ExGenRandom") + 0xe0 + 4); + if (g_Backend->VirtRead4(ExGenRandom - Gva_t(4)) != 0xf2c70f48) { + fmt::print("It seems that nt!ExGenRandom's code has changed, update the " + "offset!\n"); + return false; + } + + if (!g_Backend->SetBreakpoint(ExGenRandom, [](Backend_t *Backend) { + DebugPrint("Hit ExGenRandom!\n"); + Backend->Rdx(Backend->Rdrand()); + })) { + fmt::print("Failed to SetBreakpoint ExGenRandom\n"); + return false; + } + + // + // Catch bugchecks. + // + + if (!g_Backend->SetBreakpoint("nt!KeBugCheck2", [](Backend_t *Backend) { + const uint64_t BCode = Backend->GetArg(0); + const uint64_t B0 = Backend->GetArg(1); + const uint64_t B1 = Backend->GetArg(2); + const uint64_t B2 = Backend->GetArg(3); + const uint64_t B3 = Backend->GetArg(4); + const uint64_t B4 = Backend->GetArg(5); + const std::string Filename = + fmt::format("crash-{:#x}-{:#x}-{:#x}-{:#x}-{:#x}-{:#x}", BCode, B0, + B1, B2, B3, B4); + DebugPrint("KeBugCheck2: {}\n", Filename); + Backend->Stop(Crash_t(Filename)); + })) { + fmt::print("Failed to SetBreakpoint KeBugCheck2\n"); + return false; + } + + // + // Catch context-switches. + // + + if (!g_Backend->SetBreakpoint("nt!SwapContext", [](Backend_t *Backend) { + DebugPrint("nt!SwapContext\n"); + Backend->Stop(Cr3Change_t()); + })) { + fmt::print("Failed to SetBreakpoint SwapContext\n"); + return false; + } + + return true; +} + +// +// Register the target. +// + +Target_t Ioctl("ioctl", Init, InsertTestcase); + +} // namespace Ioctl \ No newline at end of file