From 70cb770db09505874a673057039cce8c454163cb Mon Sep 17 00:00:00 2001 From: John Doe Date: Tue, 21 Dec 2021 00:04:58 -0800 Subject: [PATCH] added test case stuff and added vmutils.. --- .gitmodules | 3 + CMakeLists.txt | 10 ++ cmake.toml | 3 +- include/vmctx.hpp | 11 +- include/vmutils.hpp | 76 +++++++++++ src/vmctx.cpp | 13 +- src/vmlocate.cpp | 2 +- src/vmutils.cpp | 211 +++++++++++++++++++++++++++++ tests/CMakeLists.txt | 18 +++ tests/cmake.toml | 1 + tests/deps/cli-parser | 1 + tests/vm_entry_test/CMakeLists.txt | 87 ++++++++++++ tests/vm_entry_test/cmake.toml | 18 +++ tests/vm_entry_test/src/main.cpp | 6 + 14 files changed, 455 insertions(+), 5 deletions(-) create mode 100644 tests/CMakeLists.txt create mode 100644 tests/cmake.toml create mode 160000 tests/deps/cli-parser create mode 100644 tests/vm_entry_test/CMakeLists.txt create mode 100644 tests/vm_entry_test/cmake.toml create mode 100644 tests/vm_entry_test/src/main.cpp diff --git a/.gitmodules b/.gitmodules index 15c31f2..dedad5f 100644 --- a/.gitmodules +++ b/.gitmodules @@ -4,3 +4,6 @@ [submodule "deps/zydis"] path = deps/zydis url = https://github.com/zyantific/zydis +[submodule "tests/deps/cli-parser"] + path = tests/deps/cli-parser + url = https://githacks.org/_xeroxz/cli-parser diff --git a/CMakeLists.txt b/CMakeLists.txt index c351b21..610b08d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -35,6 +35,16 @@ endif() add_subdirectory(deps) set(CMAKE_FOLDER ${CMKR_CMAKE_FOLDER}) +# tests +set(CMKR_CMAKE_FOLDER ${CMAKE_FOLDER}) +if(CMAKE_FOLDER) + set(CMAKE_FOLDER "${CMAKE_FOLDER}/tests") +else() + set(CMAKE_FOLDER tests) +endif() +add_subdirectory(tests) +set(CMAKE_FOLDER ${CMKR_CMAKE_FOLDER}) + # Target vmprofiler set(CMKR_TARGET vmprofiler) set(vmprofiler_SOURCES "") diff --git a/cmake.toml b/cmake.toml index e9c5b15..49a8d5a 100644 --- a/cmake.toml +++ b/cmake.toml @@ -23,4 +23,5 @@ compile-definitions = [ "NOMINMAX" ] -[subdir.deps] \ No newline at end of file +[subdir.deps] +[subdir.tests] \ No newline at end of file diff --git a/include/vmctx.hpp b/include/vmctx.hpp index 52aa86e..a35f5e1 100644 --- a/include/vmctx.hpp +++ b/include/vmctx.hpp @@ -2,7 +2,14 @@ #include namespace vm { -struct ctx_t { - const std::uintptr_t module_base, image_base, vm_entry_rva, image_size; +class vmctx_t { + public: + explicit vmctx_t(std::uintptr_t module_base, + std::uintptr_t image_base, + std::uintptr_t vm_entry_rva, + std::uintptr_t image_size); + bool init(); + const std::uintptr_t m_module_base, m_image_base, m_vm_entry_rva, + m_image_size; }; } // namespace vm \ No newline at end of file diff --git a/include/vmutils.hpp b/include/vmutils.hpp index a605799..8824495 100644 --- a/include/vmutils.hpp +++ b/include/vmutils.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include using u8 = unsigned char; @@ -39,4 +40,79 @@ inline void init() { ZYDIS_FORMATTER_STYLE_INTEL); } } + +/// +/// determines if a given decoded native instruction is a JCC... +/// +/// +/// +bool is_jmp(const zydis_decoded_instr_t& instr); + +/// +/// utils pertaining to native registers... +/// +namespace reg { +/// +/// converts say... AL to RAX... +/// +/// a zydis decoded register value... +/// returns the largest width register of the given register... AL +/// gives RAX... +zydis_register_t to64(zydis_register_t reg); + +/// +/// compares to registers with each other... calls to64 and compares... +/// +/// register a... +/// register b... +/// returns true if register to64(a) == to64(b)... +bool compare(zydis_register_t a, zydis_register_t b); +} // namespace reg + +/// +/// flatten native instruction stream, takes every JCC (follows the branch)... +/// +/// filled with decoded instructions... +/// linear virtual address to start flattening +/// from... keep JCC's in the flattened +/// instruction stream... returns true if flattened was +/// successful... +bool flatten(zydis_routine_t& routine, + std::uintptr_t routine_addr, + bool keep_jmps = false, + std::uint32_t max_instrs = 500, + std::uintptr_t module_base = 0ull); + +/// +/// deadstore deobfuscation of a flattened routine... +/// +/// reference to a flattened instruction vector... +void deobfuscate(zydis_routine_t& routine); + +/// +/// small namespace that contains function wrappers to determine the validity of +/// linear virtual addresses... +/// +namespace scn { +/// +/// determines if a pointer lands inside of a section that is readonly... +/// +/// this also checks to make sure the section is not discardable... +/// +/// linear virtual address of the module.... +/// linear virtual address +/// returns true if ptr lands inside of a readonly section of the +/// module +bool read_only(std::uint64_t module_base, std::uint64_t ptr); + +/// +/// determines if a pointer lands inside of a section that is executable... +/// +/// this also checks to make sure the section is not discardable... +/// +/// +/// +/// +bool executable(std::uint64_t module_base, std::uint64_t ptr); +} // namespace scn } // namespace vm::utils \ No newline at end of file diff --git a/src/vmctx.cpp b/src/vmctx.cpp index 0964846..82e82c0 100644 --- a/src/vmctx.cpp +++ b/src/vmctx.cpp @@ -1 +1,12 @@ -#include \ No newline at end of file +#include + +namespace vm { +vmctx_t::vmctx_t(std::uintptr_t module_base, + std::uintptr_t image_base, + std::uintptr_t vm_entry_rva, + std::uintptr_t image_size) + : m_module_base(module_base), + m_image_base(image_base), + m_vm_entry_rva(vm_entry_rva), + m_image_size(image_size) {} +} // namespace vm \ No newline at end of file diff --git a/src/vmlocate.cpp b/src/vmlocate.cpp index 6f69306..a49f647 100644 --- a/src/vmlocate.cpp +++ b/src/vmlocate.cpp @@ -50,7 +50,7 @@ std::vector get_vm_entries(std::uintptr_t module_base, PUSH_4B_IMM, PUSH_4B_MASK); zydis_routine_t rtn; - if (!scn::executable(module_base, result)) + if (!vm::utils::scn::executable(module_base, result)) continue; if (!vm::utils::flatten(rtn, result, false, 500, module_base)) diff --git a/src/vmutils.cpp b/src/vmutils.cpp index e69de29..447bf63 100644 --- a/src/vmutils.cpp +++ b/src/vmutils.cpp @@ -0,0 +1,211 @@ +#include + +namespace vm::locate { + +bool is_jmp(const zydis_decoded_instr_t& instr) { + return instr.mnemonic >= ZYDIS_MNEMONIC_JB && + instr.mnemonic <= ZYDIS_MNEMONIC_JZ; +} + +bool flatten(zydis_routine_t& routine, + std::uintptr_t routine_addr, + bool keep_jmps, + std::uint32_t max_instrs, + std::uintptr_t module_base) { + zydis_decoded_instr_t instr; + std::uint32_t instr_cnt = 0u; + + while (ZYAN_SUCCESS(ZydisDecoderDecodeBuffer( + vm::utils::g_decoder.get(), reinterpret_cast(routine_addr), 0x1000, + &instr))) { + if (++instr_cnt > max_instrs) + return false; + // detect if we have already been at this instruction... if so that means + // there is a loop and we are going to just return... + if (std::find_if(routine.begin(), routine.end(), + [&](const zydis_instr_t& zydis_instr) -> bool { + return zydis_instr.addr == routine_addr; + }) != routine.end()) + return true; + + std::vector raw_instr; + raw_instr.insert(raw_instr.begin(), (u8*)routine_addr, + (u8*)routine_addr + instr.length); + + if (is_jmp(instr) || + instr.mnemonic == ZYDIS_MNEMONIC_CALL && + instr.operands[0].type != ZYDIS_OPERAND_TYPE_REGISTER) { + if (instr.operands[0].type == ZYDIS_OPERAND_TYPE_REGISTER) { + routine.push_back({instr, raw_instr, routine_addr}); + return true; + } + + if (keep_jmps) + routine.push_back({instr, raw_instr, routine_addr}); + ZydisCalcAbsoluteAddress(&instr, &instr.operands[0], routine_addr, + &routine_addr); + } else if (instr.mnemonic == ZYDIS_MNEMONIC_RET) { + routine.push_back({instr, raw_instr, routine_addr}); + return true; + } else { + routine.push_back({instr, raw_instr, routine_addr}); + routine_addr += instr.length; + } + + // optional sanity checking... + if (module_base && !vm::utils::scn::executable(module_base, routine_addr)) + return false; + } + return false; +} + +void deobfuscate(zydis_routine_t& routine) { + static const auto _uses_reg = [](zydis_decoded_operand_t& op, + zydis_register_t reg) -> bool { + switch (op.type) { + case ZYDIS_OPERAND_TYPE_MEMORY: { + return vm::utils::reg::compare(op.mem.base, reg) || + vm::utils::reg::compare(op.mem.index, reg); + } + case ZYDIS_OPERAND_TYPE_REGISTER: { + return vm::utils::reg::compare(op.reg.value, reg); + } + default: + break; + } + return false; + }; + + static const auto _reads = [](zydis_decoded_instr_t& instr, + zydis_register_t reg) -> bool { + if (instr.operands[0].type == ZYDIS_OPERAND_TYPE_MEMORY && + vm::utils::reg::compare(instr.operands[0].mem.base, reg)) + return true; + + for (auto op_idx = 0u; op_idx < instr.operand_count; ++op_idx) + if (instr.operands[op_idx].actions & ZYDIS_OPERAND_ACTION_READ && + _uses_reg(instr.operands[op_idx], reg)) + return true; + return false; + }; + + static const auto _writes = [](zydis_decoded_instr_t& instr, + zydis_register_t reg) -> bool { + for (auto op_idx = 0u; op_idx < instr.operand_count; ++op_idx) + // if instruction writes to the specific register... + if (instr.operands[op_idx].type == ZYDIS_OPERAND_TYPE_REGISTER && + instr.operands[op_idx].actions & ZYDIS_OPERAND_ACTION_WRITE && + !(instr.operands[op_idx].actions & ZYDIS_OPERAND_ACTION_READ) && + vm::utils::reg::compare(instr.operands[op_idx].reg.value, reg)) + return true; + return false; + }; + + std::uint32_t last_size = 0u; + static const std::vector blacklist = { + ZYDIS_MNEMONIC_CLC, ZYDIS_MNEMONIC_BT, ZYDIS_MNEMONIC_TEST, + ZYDIS_MNEMONIC_CMP, ZYDIS_MNEMONIC_CMC, ZYDIS_MNEMONIC_STC}; + + static const std::vector whitelist = { + ZYDIS_MNEMONIC_PUSH, ZYDIS_MNEMONIC_POP, ZYDIS_MNEMONIC_CALL, + ZYDIS_MNEMONIC_DIV}; + + do { + last_size = routine.size(); + for (auto itr = routine.begin(); itr != routine.end(); ++itr) { + if (std::find(whitelist.begin(), whitelist.end(), itr->instr.mnemonic) != + whitelist.end()) + continue; + + if (std::find(blacklist.begin(), blacklist.end(), itr->instr.mnemonic) != + blacklist.end()) { + routine.erase(itr); + break; + } + + zydis_register_t reg = ZYDIS_REGISTER_NONE; + // look for operands with writes to a register... + for (auto op_idx = 0u; op_idx < itr->instr.operand_count; ++op_idx) + if (itr->instr.operands[op_idx].type == ZYDIS_OPERAND_TYPE_REGISTER && + itr->instr.operands[op_idx].actions & ZYDIS_OPERAND_ACTION_WRITE) + reg = vm::utils::reg::to64(itr->instr.operands[0].reg.value); + + // if this current instruction writes to a register, look ahead in the + // instruction stream to see if it gets written too before it gets read... + if (reg != ZYDIS_REGISTER_NONE) { + // find the next place that this register is written too... + auto write_result = std::find_if(itr + 1, routine.end(), + [&](zydis_instr_t& instr) -> bool { + return _writes(instr.instr, reg); + }); + + auto read_result = std::find_if(itr + 1, write_result, + [&](zydis_instr_t& instr) -> bool { + return _reads(instr.instr, reg); + }); + + // if there is neither a read or a write to this register in the + // instruction stream then we are going to be safe and leave the + // instruction in the stream... + if (read_result == routine.end() && write_result == routine.end()) + continue; + + // if there is no read of the register before the next write... and + // there is a known next write, then remove the instruction from the + // stream... + if (read_result == write_result && write_result != routine.end()) { + // if the instruction reads and writes the same register than skip... + if (_reads(read_result->instr, reg) && + _writes(read_result->instr, reg)) + continue; + + routine.erase(itr); + break; + } + } + } + } while (last_size != routine.size()); +} + +namespace reg { +zydis_register_t to64(zydis_register_t reg) { + return ZydisRegisterGetLargestEnclosing(ZYDIS_MACHINE_MODE_LONG_64, reg); +} + +bool compare(zydis_register_t a, zydis_register_t b) { + return to64(a) == to64(b); +} +} // namespace reg + +namespace scn { +bool read_only(std::uint64_t module_base, std::uint64_t ptr) { + auto win_image = reinterpret_cast*>(module_base); + auto section_count = win_image->get_file_header()->num_sections; + auto sections = win_image->get_nt_headers()->get_sections(); + + for (auto idx = 0u; idx < section_count; ++idx) + if (ptr >= sections[idx].virtual_address + module_base && + ptr < sections[idx].virtual_address + sections[idx].virtual_size + + module_base) + return !(sections[idx].characteristics.mem_discardable) && + !(sections[idx].characteristics.mem_write); + + return false; +} + +bool executable(std::uint64_t module_base, std::uint64_t ptr) { + auto win_image = reinterpret_cast*>(module_base); + auto section_count = win_image->get_file_header()->num_sections; + auto sections = win_image->get_nt_headers()->get_sections(); + + for (auto idx = 0u; idx < section_count; ++idx) + if (ptr >= sections[idx].virtual_address + module_base && + ptr < sections[idx].virtual_address + sections[idx].virtual_size + + module_base) + return !(sections[idx].characteristics.mem_discardable) && + sections[idx].characteristics.mem_execute; + + return false; +} +} // namespace scn +} // namespace vm::locate \ No newline at end of file diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..0bb8d34 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,18 @@ +# This file is automatically generated from cmake.toml - DO NOT EDIT +# See https://github.com/build-cpp/cmkr for more information + +# Create a configure-time dependency on cmake.toml to improve IDE support +if(CMKR_ROOT_PROJECT) + configure_file(cmake.toml cmake.toml COPYONLY) +endif() + +# vm_entry_test +set(CMKR_CMAKE_FOLDER ${CMAKE_FOLDER}) +if(CMAKE_FOLDER) + set(CMAKE_FOLDER "${CMAKE_FOLDER}/vm_entry_test") +else() + set(CMAKE_FOLDER vm_entry_test) +endif() +add_subdirectory(vm_entry_test) +set(CMAKE_FOLDER ${CMKR_CMAKE_FOLDER}) + diff --git a/tests/cmake.toml b/tests/cmake.toml new file mode 100644 index 0000000..e59326b --- /dev/null +++ b/tests/cmake.toml @@ -0,0 +1 @@ +[subdir.vm_entry_test] \ No newline at end of file diff --git a/tests/deps/cli-parser b/tests/deps/cli-parser new file mode 160000 index 0000000..1aedaf8 --- /dev/null +++ b/tests/deps/cli-parser @@ -0,0 +1 @@ +Subproject commit 1aedaf8bb7f383f54b7cd498767611535526da85 diff --git a/tests/vm_entry_test/CMakeLists.txt b/tests/vm_entry_test/CMakeLists.txt new file mode 100644 index 0000000..2beb3ab --- /dev/null +++ b/tests/vm_entry_test/CMakeLists.txt @@ -0,0 +1,87 @@ +# This file is automatically generated from cmake.toml - DO NOT EDIT +# See https://github.com/build-cpp/cmkr for more information + +cmake_minimum_required(VERSION 3.15) + +# Regenerate CMakeLists.txt automatically in the root project +set(CMKR_ROOT_PROJECT OFF) +if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + set(CMKR_ROOT_PROJECT ON) + + # Bootstrap cmkr + include(cmkr.cmake OPTIONAL RESULT_VARIABLE CMKR_INCLUDE_RESULT) + if(CMKR_INCLUDE_RESULT) + cmkr() + endif() + + # Enable folder support + set_property(GLOBAL PROPERTY USE_FOLDERS ON) +endif() + +# Create a configure-time dependency on cmake.toml to improve IDE support +if(CMKR_ROOT_PROJECT) + configure_file(cmake.toml cmake.toml COPYONLY) +endif() + +project(vm_entry_test) + +# Target cli-parser +set(CMKR_TARGET cli-parser) +set(cli-parser_SOURCES "") + +set(CMKR_SOURCES ${cli-parser_SOURCES}) +add_library(cli-parser INTERFACE) + +if(cli-parser_SOURCES) + target_sources(cli-parser INTERFACE ${cli-parser_SOURCES}) +endif() + +target_include_directories(cli-parser INTERFACE + "../deps/cli-parser" +) + +unset(CMKR_TARGET) +unset(CMKR_SOURCES) + +# Target vm_entry_test +set(CMKR_TARGET vm_entry_test) +set(vm_entry_test_SOURCES "") + +list(APPEND vm_entry_test_SOURCES + "src/main.cpp" +) + +list(APPEND vm_entry_test_SOURCES + cmake.toml +) + +set(CMKR_SOURCES ${vm_entry_test_SOURCES}) +add_executable(vm_entry_test) + +if(vm_entry_test_SOURCES) + target_sources(vm_entry_test PRIVATE ${vm_entry_test_SOURCES}) +endif() + +get_directory_property(CMKR_VS_STARTUP_PROJECT DIRECTORY ${PROJECT_SOURCE_DIR} DEFINITION VS_STARTUP_PROJECT) +if(NOT CMKR_VS_STARTUP_PROJECT) + set_property(DIRECTORY ${PROJECT_SOURCE_DIR} PROPERTY VS_STARTUP_PROJECT vm_entry_test) +endif() + +source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${vm_entry_test_SOURCES}) + +target_compile_definitions(vm_entry_test PRIVATE + NOMINMAX +) + +target_compile_features(vm_entry_test PRIVATE + cxx_std_20 +) + +target_link_libraries(vm_entry_test PRIVATE + vmprofiler + cli-parser +) + +unset(CMKR_TARGET) +unset(CMKR_SOURCES) + diff --git a/tests/vm_entry_test/cmake.toml b/tests/vm_entry_test/cmake.toml new file mode 100644 index 0000000..d145866 --- /dev/null +++ b/tests/vm_entry_test/cmake.toml @@ -0,0 +1,18 @@ +[target.cli-parser] +type = "interface" +include-directories = ["../deps/cli-parser"] + +[project] +name = "vm_entry_test" + +[target.vm_entry_test] +type = "executable" +compile-features = ["cxx_std_20"] + +sources = [ + "src/**.cpp", + "include/**.hpp" +] + +link-libraries = ["vmprofiler", "cli-parser"] +compile-definitions = ["NOMINMAX"] \ No newline at end of file diff --git a/tests/vm_entry_test/src/main.cpp b/tests/vm_entry_test/src/main.cpp new file mode 100644 index 0000000..7fdbc06 --- /dev/null +++ b/tests/vm_entry_test/src/main.cpp @@ -0,0 +1,6 @@ +#include +#include + +int __cdecl main(int argc, char* argv[]) { + +} \ No newline at end of file