diff --git a/.gitmodules b/.gitmodules index ef4e931..676e438 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "dependencies/xtils"] path = dependencies/xtils url = https://githacks.org/_xeroxz/xtils.git +[submodule "dependencies/linux-pe"] + path = dependencies/linux-pe + url = https://github.com/can1357/linux-pe.git diff --git a/CMakeLists.txt b/CMakeLists.txt index a46e41e..3620be9 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -41,6 +41,8 @@ set(vmprofiler-cli_SOURCES "") list(APPEND vmprofiler-cli_SOURCES "src/main.cpp" + "src/vmlocate.cpp" + "include/vmlocate.hpp" "src/icon.rc" ) @@ -62,10 +64,15 @@ endif() source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${vmprofiler-cli_SOURCES}) +target_include_directories(vmprofiler-cli PRIVATE + include +) + target_link_libraries(vmprofiler-cli PRIVATE cli-parser xtils vmprofiler + linux-pe ) unset(CMKR_TARGET) diff --git a/cmake.toml b/cmake.toml index 4c57847..591d6b5 100644 --- a/cmake.toml +++ b/cmake.toml @@ -7,10 +7,16 @@ name = "vmprofiler-cli" type = "executable" sources = [ "src/main.cpp", - "src/icon.rc", + "src/vmlocate.cpp", + "include/vmlocate.hpp", + "src/icon.rc" +] +include-directories = [ + "include", ] link-libraries = [ "cli-parser", "xtils", "vmprofiler", + "linux-pe" ] \ No newline at end of file diff --git a/dependencies/CMakeLists.txt b/dependencies/CMakeLists.txt index 2f81bf4..3fb13b1 100644 --- a/dependencies/CMakeLists.txt +++ b/dependencies/CMakeLists.txt @@ -34,6 +34,24 @@ target_include_directories(cli-parser INTERFACE unset(CMKR_TARGET) unset(CMKR_SOURCES) +# Target linux-pe +set(CMKR_TARGET linux-pe) +set(linux-pe_SOURCES "") + +set(CMKR_SOURCES ${linux-pe_SOURCES}) +add_library(linux-pe INTERFACE) + +if(linux-pe_SOURCES) + target_sources(linux-pe INTERFACE ${linux-pe_SOURCES}) +endif() + +target_include_directories(linux-pe INTERFACE + "linux-pe/includes/" +) + +unset(CMKR_TARGET) +unset(CMKR_SOURCES) + # Target xtils set(CMKR_TARGET xtils) set(xtils_SOURCES "") diff --git a/dependencies/cmake.toml b/dependencies/cmake.toml index 875ccdb..e19d630 100644 --- a/dependencies/cmake.toml +++ b/dependencies/cmake.toml @@ -4,6 +4,10 @@ type = "interface" include-directories = ["cli-parser"] +[target.linux-pe] +type = "interface" +include-directories = ["linux-pe/includes/"] + [target.xtils] type = "interface" include-directories = ["xtils"] diff --git a/dependencies/linux-pe b/dependencies/linux-pe new file mode 160000 index 0000000..db2b7af --- /dev/null +++ b/dependencies/linux-pe @@ -0,0 +1 @@ +Subproject commit db2b7af6e6beae1bc391ff8f8e5c97b963dc3258 diff --git a/dependencies/vmprofiler b/dependencies/vmprofiler index e8eb794..7240a2a 160000 --- a/dependencies/vmprofiler +++ b/dependencies/vmprofiler @@ -1 +1 @@ -Subproject commit e8eb794dc1c853807b406815e308f3d201fa2a93 +Subproject commit 7240a2a23c7d789d5973de04458c7ebca16a81d0 diff --git a/include/vmlocate.hpp b/include/vmlocate.hpp new file mode 100644 index 0000000..da957e5 --- /dev/null +++ b/include/vmlocate.hpp @@ -0,0 +1,25 @@ +#pragma once +#include +#include +#include +#include + +#define ABS_TO_IMG( addr, mod_base, img_base ) ( addr - mod_base ) + img_base +#define LEA_R12_SIG "\x4C\x8D\x25\x00\x00\x00\x00" +#define LEA_R12_MASK "xxx????" + +#define PUSH_4B_IMM "\x68\x00\x00\x00\x00" +#define PUSH_4B_MASK "x????" + +namespace vm::locate +{ + struct vm_handler_table_info_t + { + std::uint32_t rva, lea_r12_rva; + zydis_decoded_instr_t lea_r12_instr; + }; + + std::vector< vm_handler_table_info_t > all_handler_tables( std::uintptr_t module_base ); + std::vector< std::pair< std::uint32_t, std::uint32_t > > all_vm_enters( + std::uintptr_t module_base, std::vector< vm_handler_table_info_t > &vm_handler_tables ); +} // namespace vm::locate \ No newline at end of file diff --git a/src/icon.aps b/src/icon.aps new file mode 100644 index 0000000..f4667c4 Binary files /dev/null and b/src/icon.aps differ diff --git a/src/main.cpp b/src/main.cpp index cbeced4..57097bd 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,16 +1,12 @@ #include +#include #include #include #include #include -#include #include -#include -#include -#include - -#define ABS_TO_IMG( addr, mod_base, img_base ) ( addr - mod_base ) + img_base +#include int __cdecl main( int argc, const char *argv[] ) { @@ -21,6 +17,12 @@ int __cdecl main( int argc, const char *argv[] ) parser.add_argument().name( "--showhandlers" ).description( "show all vm handlers..." ); parser.add_argument().name( "--showhandler" ).description( "show a specific vm handler given its index..." ); parser.add_argument().name( "--vmp2file" ).description( "path to .vmp2 file..." ); + parser.add_argument() + .name( "--indexes" ) + .description( "displays vm handler table indexes for a given vm handler name such as 'READQ', or 'WRITEQ'..." ); + parser.add_argument() + .name( "--scanfortables" ) + .description( "scans all executable sections for vm handler tables..." ); parser.add_argument() .name( "--showblockinstrs" ) .description( "show the virtual instructions of a specific code block..." ); @@ -45,6 +47,32 @@ int __cdecl main( int argc, const char *argv[] ) return 0; } + if ( parser.exists( "bin" ) && parser.exists( "scanfortables" ) ) + { + if ( !std::filesystem::exists( parser.get< std::string >( "bin" ) ) ) + { + std::printf( "> path to protected file is invalid... check your cli args...\n" ); + return -1; + } + + const auto module_base = reinterpret_cast< std::uintptr_t >( + LoadLibraryExA( parser.get< std::string >( "bin" ).c_str(), NULL, DONT_RESOLVE_DLL_REFERENCES ) ); + + auto result = vm::locate::all_handler_tables( module_base ); + std::vector< std::uint32_t > handler_table_rvas; + + std::for_each( result.begin(), result.end(), [ & ]( const vm::locate::vm_handler_table_info_t &data ) { + if ( std::find( handler_table_rvas.begin(), handler_table_rvas.end(), data.rva ) == + handler_table_rvas.end() ) + handler_table_rvas.push_back( data.rva ); + } ); + + std::printf( "{\n" ); + for ( auto &table_rva : handler_table_rvas ) + std::printf( "\t0x%x,\n", table_rva ); + std::printf( "}\n" ); + } + if ( parser.exists( "bin" ) && parser.exists( "vmentry" ) ) { if ( !std::filesystem::exists( parser.get< std::string >( "bin" ) ) ) @@ -140,6 +168,16 @@ int __cdecl main( int argc, const char *argv[] ) } std::puts( "\n" ); } + else if ( parser.exists( "indexes" ) ) + { + const auto handler_name = parser.get< std::string >( "indexes" ); + std::printf( "{\n" ); + for ( auto idx = 0u; idx < vmctx.vm_handlers.size(); ++idx ) + if ( vmctx.vm_handlers[ idx ].profile && + !strcmp( vmctx.vm_handlers[ idx ].profile->name, handler_name.c_str() ) ) + std::printf( "\t0x%x,\n", idx ); + std::printf( "}\n" ); + } } if ( !parser.exists( "vmp2file" ) ) diff --git a/src/vmlocate.cpp b/src/vmlocate.cpp new file mode 100644 index 0000000..7a64720 --- /dev/null +++ b/src/vmlocate.cpp @@ -0,0 +1,264 @@ +#include + +namespace vm::locate +{ + std::vector< vm_handler_table_info_t > all_handler_tables( std::uintptr_t module_base ) + { + std::vector< vm_handler_table_info_t > result; + auto module_info = reinterpret_cast< win::image_t<> * >( module_base ); + auto sections = module_info->get_nt_headers()->get_sections(); + auto num_sections = module_info->get_file_header()->num_sections; + auto umtils = xtils::um_t::get_instance(); + + static const auto lea_r12_validate = []( std::uintptr_t addr ) -> bool { + ZydisDecodedInstruction instr; + ZydisDecoder decoder; + ZydisRegister jmp_reg = ZYDIS_REGISTER_NONE; + ZydisDecoderInit( &decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_ADDRESS_WIDTH_64 ); + + unsigned instr_count = 0u; + bool found_table_idx = false, found_valid_jmp = false; + + while ( ZYAN_SUCCESS( + ZydisDecoderDecodeBuffer( &decoder, reinterpret_cast< void * >( addr ), 0x1000, &instr ) ) ) + { + ++instr_count; + if ( instr_count >= 0x1000 || + instr.mnemonic == ZYDIS_MNEMONIC_INVALID ) // prevent run offs and misalignment... + break; + + // determine if we are looking at a JMP RCX/JMP RDX... + if ( vm::util::is_jmp( instr ) ) + { + if ( instr.operands[ 0 ].type == ZYDIS_OPERAND_TYPE_REGISTER && + ( instr.operands[ 0 ].reg.value == ZYDIS_REGISTER_RDX || + instr.operands[ 0 ].reg.value == ZYDIS_REGISTER_RCX ) ) + { + if ( jmp_reg == ZYDIS_REGISTER_NONE || jmp_reg != instr.operands[ 0 ].reg.value ) + break; + + // else we set it to true and break... + found_valid_jmp = true; + break; + } + + // take JCC branch no matter what... + ZydisCalcAbsoluteAddress( &instr, &instr.operands[ 0 ], addr, &addr ); + + // dont execute anymore address advancement code... + continue; + } + // else if the instruction is a MOV RDX/RCX, [R12+RAX*0x8]... we know this is an index into + // the vm handler table and thus this lea r12, xxxx is probably legit... + else if ( instr.mnemonic == ZYDIS_MNEMONIC_MOV && + instr.operands[ 0 ].type == ZYDIS_OPERAND_TYPE_REGISTER && + ( instr.operands[ 0 ].reg.value == ZYDIS_REGISTER_RDX || + instr.operands[ 0 ].reg.value == ZYDIS_REGISTER_RCX ) && + instr.operands[ 1 ].type == ZYDIS_OPERAND_TYPE_MEMORY && + instr.operands[ 1 ].mem.base == ZYDIS_REGISTER_R12 && + instr.operands[ 1 ].mem.index == ZYDIS_REGISTER_RAX && instr.operands[ 1 ].mem.scale == 0x8 ) + { + found_table_idx = true; + jmp_reg = instr.operands[ 0 ].reg.value; + } + else if ( instr.mnemonic == ZYDIS_MNEMONIC_RET || instr.mnemonic == ZYDIS_MNEMONIC_CALL ) + break; + + // advance the instruction address "addr"... + addr += instr.length; + } + + return found_table_idx && found_valid_jmp; + }; + + ZydisDecodedInstruction instr; + ZydisDecoder decoder; + ZydisDecoderInit( &decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_ADDRESS_WIDTH_64 ); + + for ( auto idx = 0u; idx < num_sections; ++idx ) + { + // if the section is executable and not discardable... scan it for lea r12, xxxx and ensure its loading + // a virtual machine handler table into r12... + if ( sections[ idx ].characteristics.mem_execute && !sections[ idx ].characteristics.mem_discardable ) + { + void *scan_result = reinterpret_cast< void * >( sections[ idx ].virtual_address + module_base ); + + do + { + // compute how far away from the beginning of the section we are... + auto section_size = reinterpret_cast< std::uintptr_t >( scan_result ) - + ( sections[ idx ].virtual_address + module_base ); + + // scan from the last scans result to the end of the section for lea r12's... + scan_result = umtils->sigscan( scan_result, sections[ idx ].virtual_size - section_size, + LEA_R12_SIG, LEA_R12_MASK ); + + if ( scan_result ) + { + // check to see if we are looking at: + // 1.) a legit "lea r12, xxxx" and not a misaligned instruction... + // 2.) if the instruction stream is followed in zydis it ends with a jmp rcx/jmp rdx... + if ( lea_r12_validate( reinterpret_cast< std::uintptr_t >( scan_result ) ) ) + { + if ( ZYAN_SUCCESS( ZydisDecoderDecodeBuffer( + &decoder, scan_result, sections[ idx ].virtual_size - section_size, &instr ) ) ) + { + vm_handler_table_info_t vm_handler_table_info; + vm_handler_table_info.rva = + ( instr.operands[ 1 ].mem.disp.value + + reinterpret_cast< std::uintptr_t >( scan_result ) + instr.length ) - + module_base; + + vm_handler_table_info.lea_r12_rva = + reinterpret_cast< std::uintptr_t >( scan_result ) - module_base; + + vm_handler_table_info.lea_r12_instr = instr; + result.push_back( vm_handler_table_info ); + } + } + + scan_result = reinterpret_cast< void * >( reinterpret_cast< std::uintptr_t >( scan_result ) + + sizeof LEA_R12_SIG ); + } + + } while ( scan_result ); + } + } + return result; + } + + std::vector< std::pair< std::uint32_t, std::uint32_t > > all_vm_enters( + std::uintptr_t module_base, std::vector< vm_handler_table_info_t > &vm_handler_tables ) + { + std::vector< std::pair< std::uint32_t, std::uint32_t > > result; + auto module_info = reinterpret_cast< win::image_t<> * >( module_base ); + auto sections = module_info->get_nt_headers()->get_sections(); + auto num_sections = module_info->get_file_header()->num_sections; + auto umtils = xtils::um_t::get_instance(); + auto module_end = module_base + module_info->get_nt_headers()->optional_header.size_image; + + static const auto validate_vm_enter = [ & ]( std::uintptr_t addr ) -> bool { + ZydisDecodedInstruction instr; + ZydisDecoder decoder; + ZydisDecoderInit( &decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_ADDRESS_WIDTH_64 ); + + unsigned instr_count = 0u; + bool found_valid_jmp = false; + std::vector< ZydisDecodedInstruction > instr_stream; + + while ( addr >= module_base && addr < module_end && + ZYAN_SUCCESS( + ZydisDecoderDecodeBuffer( &decoder, reinterpret_cast< void * >( addr ), 0x1000, &instr ) ) ) + { + if ( !instr_count && instr.mnemonic != ZYDIS_MNEMONIC_PUSH ) + break; + + ++instr_count; // handle run offs and misaligned instructions... + if ( instr_count > 500 || instr.mnemonic == ZYDIS_MNEMONIC_INVALID || + instr.mnemonic == ZYDIS_MNEMONIC_RET || instr.mnemonic == ZYDIS_MNEMONIC_CALL ) + return false; + + // determine if we are looking at a JMP RCX/JMP RDX... + if ( vm::util::is_jmp( instr ) ) + { + if ( instr.operands[ 0 ].type == ZYDIS_OPERAND_TYPE_REGISTER && + ( instr.operands[ 0 ].reg.value == ZYDIS_REGISTER_RDX || + instr.operands[ 0 ].reg.value == ZYDIS_REGISTER_RCX ) ) + { + + // else we set it to true and break... + found_valid_jmp = true; + instr_stream.push_back( instr ); + break; + } + + // take JCC branch no matter what... + ZydisCalcAbsoluteAddress( &instr, &instr.operands[ 0 ], addr, &addr ); + + // dont execute anymore address advancement code... + continue; + } + + instr_stream.push_back( instr ); + addr += instr.length; + } + + if ( !found_valid_jmp ) + return false; + + // second instruction in the flattened stream should be a push... + // this is also an optimization so we dont have to hit that 0^2 std::find_if every time... + if ( instr_stream[ 1 ].mnemonic != ZYDIS_MNEMONIC_PUSH ) + return false; + + if ( std::find_if( instr_stream.begin() + 1, instr_stream.end(), + [ & ]( const ZydisDecodedInstruction &instr ) { + return instr.mnemonic == ZYDIS_MNEMONIC_PUSH && + instr.operands[ 0 ].type == ZYDIS_OPERAND_TYPE_IMMEDIATE; + } ) == instr_stream.end() ) + return false; + + // scan over the instruction stream to see if it contains an lea r12, xxxx which is a known vm handler table + // load into r12... this is O^2 and very slow... whatever... + return std::find_if( + instr_stream.begin(), instr_stream.end(), [ & ]( const ZydisDecodedInstruction &instr ) { + return instr.mnemonic == ZYDIS_MNEMONIC_LEA && + instr.operands[ 1 ].type == ZYDIS_OPERAND_TYPE_MEMORY && + std::find_if( vm_handler_tables.begin(), vm_handler_tables.end(), + [ & ]( const vm_handler_table_info_t &table_info ) { + return table_info.lea_r12_instr.operands[ 1 ].mem.disp.value == + instr.operands[ 1 ].mem.disp.value; + } ) != vm_handler_tables.end(); + } ) != instr_stream.end(); + }; + + ZydisDecodedInstruction instr; + ZydisDecoder decoder; + ZydisDecoderInit( &decoder, ZYDIS_MACHINE_MODE_LONG_64, ZYDIS_ADDRESS_WIDTH_64 ); + + // for each section... + for ( auto idx = 0u; idx < num_sections; ++idx ) + { + // we are only interested in executable (non-discardable) sections... + if ( sections[ idx ].characteristics.mem_execute && !sections[ idx ].characteristics.mem_discardable ) + { + void *scan_result = reinterpret_cast< void * >( sections[ idx ].virtual_address + module_base ); + + do + { + // compute how far away from the beginning of the section we are... + auto section_size = reinterpret_cast< std::uintptr_t >( scan_result ) - + ( sections[ idx ].virtual_address + module_base ); + + if ( section_size > sections[ idx ].virtual_size ) + break; + + scan_result = umtils->sigscan( scan_result, sections[ idx ].virtual_size - section_size, + PUSH_4B_IMM, PUSH_4B_MASK ); + + if ( scan_result ) + { + if ( validate_vm_enter( reinterpret_cast< std::uintptr_t >( scan_result ) ) && + ZYAN_SUCCESS( ZydisDecoderDecodeBuffer( &decoder, scan_result, 0x1000, &instr ) ) ) + { + if ( std::find_if( result.begin(), result.end(), + [ & ]( const std::pair< std::uint32_t, std::uint32_t > &info ) { + return info.second == instr.operands[ 0 ].imm.value.u; + } ) == result.end() ) + { + result.push_back( { reinterpret_cast< std::uintptr_t >( scan_result ) - module_base, + instr.operands[ 0 ].imm.value.u } ); + } + } + + scan_result = reinterpret_cast< void * >( reinterpret_cast< std::uintptr_t >( scan_result ) + + sizeof PUSH_4B_IMM ); + } + + } while ( scan_result ); + } + } + + return result; + } +} // namespace vm::locate \ No newline at end of file