parent
a9c482fb13
commit
786ecb2f81
@ -1,18 +1,5 @@
|
||||
---
|
||||
BasedOnStyle: Microsoft
|
||||
AlignAfterOpenBracket: Align
|
||||
AllowAllArgumentsOnNextLine: 'true'
|
||||
AllowAllParametersOfDeclarationOnNextLine: 'true'
|
||||
AllowShortIfStatementsOnASingleLine: Never
|
||||
BreakBeforeBraces: Allman
|
||||
IndentWidth: '4'
|
||||
Language: Cpp
|
||||
NamespaceIndentation: All
|
||||
SpacesInAngles: 'true'
|
||||
SpacesInCStyleCastParentheses: 'true'
|
||||
SpacesInContainerLiterals: 'true'
|
||||
SpacesInParentheses: 'true'
|
||||
SpacesInSquareBrackets: 'true'
|
||||
UseTab: Never
|
||||
BasedOnStyle: Chromium
|
||||
|
||||
...
|
||||
|
||||
|
@ -0,0 +1,90 @@
|
||||
# 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(vmassembler)
|
||||
|
||||
# dependencies
|
||||
set(CMKR_CMAKE_FOLDER ${CMAKE_FOLDER})
|
||||
if(CMAKE_FOLDER)
|
||||
set(CMAKE_FOLDER "${CMAKE_FOLDER}/dependencies")
|
||||
else()
|
||||
set(CMAKE_FOLDER dependencies)
|
||||
endif()
|
||||
add_subdirectory(dependencies)
|
||||
set(CMAKE_FOLDER ${CMKR_CMAKE_FOLDER})
|
||||
|
||||
# Target vmassembler
|
||||
set(CMKR_TARGET vmassembler)
|
||||
set(vmassembler_SOURCES "")
|
||||
|
||||
list(APPEND vmassembler_SOURCES
|
||||
"src/compiler.cpp"
|
||||
"src/main.cpp"
|
||||
"src/parser.cpp"
|
||||
"include/compiler.hpp"
|
||||
"include/gen_code.hpp"
|
||||
"include/parser.hpp"
|
||||
)
|
||||
|
||||
list(APPEND vmassembler_SOURCES
|
||||
cmake.toml
|
||||
)
|
||||
|
||||
set(CMKR_SOURCES ${vmassembler_SOURCES})
|
||||
add_executable(vmassembler)
|
||||
|
||||
if(vmassembler_SOURCES)
|
||||
target_sources(vmassembler PRIVATE ${vmassembler_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 vmassembler)
|
||||
endif()
|
||||
|
||||
source_group(TREE ${CMAKE_CURRENT_SOURCE_DIR} FILES ${vmassembler_SOURCES})
|
||||
|
||||
target_compile_definitions(vmassembler PRIVATE
|
||||
NOMINMAX
|
||||
)
|
||||
|
||||
target_compile_features(vmassembler PRIVATE
|
||||
cxx_std_20
|
||||
)
|
||||
|
||||
target_include_directories(vmassembler PRIVATE
|
||||
include
|
||||
)
|
||||
|
||||
target_link_libraries(vmassembler PRIVATE
|
||||
vmprofiler
|
||||
xtils
|
||||
cli-parser
|
||||
linux-pe
|
||||
)
|
||||
|
||||
unset(CMKR_TARGET)
|
||||
unset(CMKR_SOURCES)
|
||||
|
@ -0,0 +1,12 @@
|
||||
[project]
|
||||
name = "vmassembler"
|
||||
|
||||
[subdir.dependencies]
|
||||
|
||||
[target.vmassembler]
|
||||
type = "executable"
|
||||
compile-features = ["cxx_std_20"]
|
||||
sources = ["src/**.cpp", "include/**.hpp"]
|
||||
include-directories = ["include"]
|
||||
link-libraries = ["vmprofiler", "xtils", "cli-parser", "linux-pe"]
|
||||
compile-definitions = ["NOMINMAX"]
|
@ -0,0 +1,162 @@
|
||||
include_guard()
|
||||
|
||||
# Change these defaults to point to your infrastructure if desired
|
||||
set(CMKR_REPO "https://github.com/build-cpp/cmkr" CACHE STRING "cmkr git repository" FORCE)
|
||||
set(CMKR_TAG "archive_264e4ace" CACHE STRING "cmkr git tag (this needs to be available forever)" FORCE)
|
||||
|
||||
# Set these from the command line to customize for development/debugging purposes
|
||||
set(CMKR_EXECUTABLE "" CACHE FILEPATH "cmkr executable")
|
||||
set(CMKR_SKIP_GENERATION OFF CACHE BOOL "skip automatic cmkr generation")
|
||||
|
||||
# Disable cmkr if generation is disabled
|
||||
if(DEFINED ENV{CI} OR CMKR_SKIP_GENERATION)
|
||||
message(STATUS "[cmkr] Skipping automatic cmkr generation")
|
||||
macro(cmkr)
|
||||
endmacro()
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Disable cmkr if no cmake.toml file is found
|
||||
if(NOT EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/cmake.toml")
|
||||
message(AUTHOR_WARNING "[cmkr] Not found: ${CMAKE_CURRENT_SOURCE_DIR}/cmake.toml")
|
||||
macro(cmkr)
|
||||
endmacro()
|
||||
return()
|
||||
endif()
|
||||
|
||||
# Convert a Windows native path to CMake path
|
||||
if(CMKR_EXECUTABLE MATCHES "\\\\")
|
||||
string(REPLACE "\\" "/" CMKR_EXECUTABLE_CMAKE "${CMKR_EXECUTABLE}")
|
||||
set(CMKR_EXECUTABLE "${CMKR_EXECUTABLE_CMAKE}" CACHE FILEPATH "" FORCE)
|
||||
unset(CMKR_EXECUTABLE_CMAKE)
|
||||
endif()
|
||||
|
||||
# Helper macro to execute a process (COMMAND_ERROR_IS_FATAL ANY is 3.19 and higher)
|
||||
function(cmkr_exec)
|
||||
execute_process(COMMAND ${ARGV} RESULT_VARIABLE CMKR_EXEC_RESULT)
|
||||
if(NOT CMKR_EXEC_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "cmkr_exec(${ARGV}) failed (exit code ${CMKR_EXEC_RESULT})")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# Windows-specific hack (CMAKE_EXECUTABLE_PREFIX is not set at the moment)
|
||||
if(WIN32)
|
||||
set(CMKR_EXECUTABLE_NAME "cmkr.exe")
|
||||
else()
|
||||
set(CMKR_EXECUTABLE_NAME "cmkr")
|
||||
endif()
|
||||
|
||||
# Use cached cmkr if found
|
||||
set(CMKR_DIRECTORY "${CMAKE_CURRENT_BINARY_DIR}/_cmkr_${CMKR_TAG}")
|
||||
set(CMKR_CACHED_EXECUTABLE "${CMKR_DIRECTORY}/bin/${CMKR_EXECUTABLE_NAME}")
|
||||
|
||||
if(NOT CMKR_CACHED_EXECUTABLE STREQUAL CMKR_EXECUTABLE AND CMKR_EXECUTABLE MATCHES "^${CMAKE_CURRENT_BINARY_DIR}/_cmkr")
|
||||
message(AUTHOR_WARNING "[cmkr] Upgrading '${CMKR_EXECUTABLE}' to '${CMKR_CACHED_EXECUTABLE}'")
|
||||
unset(CMKR_EXECUTABLE CACHE)
|
||||
endif()
|
||||
|
||||
if(CMKR_EXECUTABLE AND EXISTS "${CMKR_EXECUTABLE}")
|
||||
message(VERBOSE "[cmkr] Found cmkr: '${CMKR_EXECUTABLE}'")
|
||||
elseif(CMKR_EXECUTABLE AND NOT CMKR_EXECUTABLE STREQUAL CMKR_CACHED_EXECUTABLE)
|
||||
message(FATAL_ERROR "[cmkr] '${CMKR_EXECUTABLE}' not found")
|
||||
else()
|
||||
set(CMKR_EXECUTABLE "${CMKR_CACHED_EXECUTABLE}" CACHE FILEPATH "Full path to cmkr executable" FORCE)
|
||||
message(VERBOSE "[cmkr] Bootstrapping '${CMKR_EXECUTABLE}'")
|
||||
|
||||
message(STATUS "[cmkr] Fetching cmkr...")
|
||||
if(EXISTS "${CMKR_DIRECTORY}")
|
||||
cmkr_exec("${CMAKE_COMMAND}" -E rm -rf "${CMKR_DIRECTORY}")
|
||||
endif()
|
||||
find_package(Git QUIET REQUIRED)
|
||||
cmkr_exec("${GIT_EXECUTABLE}"
|
||||
clone
|
||||
--config advice.detachedHead=false
|
||||
--branch ${CMKR_TAG}
|
||||
--depth 1
|
||||
${CMKR_REPO}
|
||||
"${CMKR_DIRECTORY}"
|
||||
)
|
||||
message(STATUS "[cmkr] Building cmkr...")
|
||||
cmkr_exec("${CMAKE_COMMAND}"
|
||||
--no-warn-unused-cli
|
||||
"${CMKR_DIRECTORY}"
|
||||
"-B${CMKR_DIRECTORY}/build"
|
||||
"-DCMAKE_BUILD_TYPE=Release"
|
||||
"-DCMAKE_INSTALL_PREFIX=${CMKR_DIRECTORY}"
|
||||
"-DCMKR_GENERATE_DOCUMENTATION=OFF"
|
||||
)
|
||||
cmkr_exec("${CMAKE_COMMAND}"
|
||||
--build "${CMKR_DIRECTORY}/build"
|
||||
--config Release
|
||||
--parallel
|
||||
)
|
||||
cmkr_exec("${CMAKE_COMMAND}"
|
||||
--install "${CMKR_DIRECTORY}/build"
|
||||
--config Release
|
||||
--prefix "${CMKR_DIRECTORY}"
|
||||
--component cmkr
|
||||
)
|
||||
if(NOT EXISTS ${CMKR_EXECUTABLE})
|
||||
message(FATAL_ERROR "[cmkr] Failed to bootstrap '${CMKR_EXECUTABLE}'")
|
||||
endif()
|
||||
cmkr_exec("${CMKR_EXECUTABLE}" version)
|
||||
message(STATUS "[cmkr] Bootstrapped ${CMKR_EXECUTABLE}")
|
||||
endif()
|
||||
execute_process(COMMAND "${CMKR_EXECUTABLE}" version
|
||||
RESULT_VARIABLE CMKR_EXEC_RESULT
|
||||
)
|
||||
if(NOT CMKR_EXEC_RESULT EQUAL 0)
|
||||
message(FATAL_ERROR "[cmkr] Failed to get version, try clearing the cache and rebuilding")
|
||||
endif()
|
||||
|
||||
# This is the macro that contains black magic
|
||||
macro(cmkr)
|
||||
# When this macro is called from the generated file, fake some internal CMake variables
|
||||
get_source_file_property(CMKR_CURRENT_LIST_FILE "${CMAKE_CURRENT_LIST_FILE}" CMKR_CURRENT_LIST_FILE)
|
||||
if(CMKR_CURRENT_LIST_FILE)
|
||||
set(CMAKE_CURRENT_LIST_FILE "${CMKR_CURRENT_LIST_FILE}")
|
||||
get_filename_component(CMAKE_CURRENT_LIST_DIR "${CMAKE_CURRENT_LIST_FILE}" DIRECTORY)
|
||||
endif()
|
||||
|
||||
# File-based include guard (include_guard is not documented to work)
|
||||
get_source_file_property(CMKR_INCLUDE_GUARD "${CMAKE_CURRENT_LIST_FILE}" CMKR_INCLUDE_GUARD)
|
||||
if(NOT CMKR_INCLUDE_GUARD)
|
||||
set_source_files_properties("${CMAKE_CURRENT_LIST_FILE}" PROPERTIES CMKR_INCLUDE_GUARD TRUE)
|
||||
|
||||
file(SHA256 "${CMAKE_CURRENT_LIST_FILE}" CMKR_LIST_FILE_SHA256_PRE)
|
||||
|
||||
# Generate CMakeLists.txt
|
||||
cmkr_exec("${CMKR_EXECUTABLE}" gen
|
||||
WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
|
||||
)
|
||||
|
||||
file(SHA256 "${CMAKE_CURRENT_LIST_FILE}" CMKR_LIST_FILE_SHA256_POST)
|
||||
|
||||
# Delete the temporary file if it was left for some reason
|
||||
set(CMKR_TEMP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/CMakerLists.txt")
|
||||
if(EXISTS "${CMKR_TEMP_FILE}")
|
||||
file(REMOVE "${CMKR_TEMP_FILE}")
|
||||
endif()
|
||||
|
||||
if(NOT CMKR_LIST_FILE_SHA256_PRE STREQUAL CMKR_LIST_FILE_SHA256_POST)
|
||||
# Copy the now-generated CMakeLists.txt to CMakerLists.txt
|
||||
# This is done because you cannot include() a file you are currently in
|
||||
configure_file(CMakeLists.txt "${CMKR_TEMP_FILE}" COPYONLY)
|
||||
|
||||
# Add the macro required for the hack at the start of the cmkr macro
|
||||
set_source_files_properties("${CMKR_TEMP_FILE}" PROPERTIES
|
||||
CMKR_CURRENT_LIST_FILE "${CMAKE_CURRENT_LIST_FILE}"
|
||||
)
|
||||
|
||||
# 'Execute' the newly-generated CMakeLists.txt
|
||||
include("${CMKR_TEMP_FILE}")
|
||||
|
||||
# Delete the generated file
|
||||
file(REMOVE "${CMKR_TEMP_FILE}")
|
||||
|
||||
# Do not execute the rest of the original CMakeLists.txt
|
||||
return()
|
||||
endif()
|
||||
# Resume executing the unmodified CMakeLists.txt
|
||||
endif()
|
||||
endmacro()
|
@ -0,0 +1,54 @@
|
||||
# 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()
|
||||
|
||||
# vmprofiler
|
||||
set(CMKR_CMAKE_FOLDER ${CMAKE_FOLDER})
|
||||
if(CMAKE_FOLDER)
|
||||
set(CMAKE_FOLDER "${CMAKE_FOLDER}/vmprofiler")
|
||||
else()
|
||||
set(CMAKE_FOLDER vmprofiler)
|
||||
endif()
|
||||
add_subdirectory(vmprofiler)
|
||||
set(CMAKE_FOLDER ${CMKR_CMAKE_FOLDER})
|
||||
|
||||
# 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
|
||||
cli-parser
|
||||
)
|
||||
|
||||
unset(CMKR_TARGET)
|
||||
unset(CMKR_SOURCES)
|
||||
|
||||
# Target xtils
|
||||
set(CMKR_TARGET xtils)
|
||||
set(xtils_SOURCES "")
|
||||
|
||||
set(CMKR_SOURCES ${xtils_SOURCES})
|
||||
add_library(xtils INTERFACE)
|
||||
|
||||
if(xtils_SOURCES)
|
||||
target_sources(xtils INTERFACE ${xtils_SOURCES})
|
||||
endif()
|
||||
|
||||
target_include_directories(xtils INTERFACE
|
||||
xtils
|
||||
)
|
||||
|
||||
unset(CMKR_TARGET)
|
||||
unset(CMKR_SOURCES)
|
||||
|
@ -0,0 +1,9 @@
|
||||
[subdir.vmprofiler]
|
||||
|
||||
[target.cli-parser]
|
||||
type = "interface"
|
||||
include-directories = ["cli-parser"]
|
||||
|
||||
[target.xtils]
|
||||
type = "interface"
|
||||
include-directories = ["xtils"]
|
@ -1 +1 @@
|
||||
Subproject commit 0f6ba9bad30d67f25f01b6c1e872077efdff61d4
|
||||
Subproject commit 14cc72c0ff121d13f2ba26be34d1e99492aad296
|
@ -1 +1 @@
|
||||
Subproject commit 7c32517322c29a866cbb1e67fb9051efa2e05553
|
||||
Subproject commit db7526d989bdfecb6fac2079929efce94cead52c
|
@ -0,0 +1,131 @@
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
#include <algorithm>
|
||||
#include <vmprofiler.hpp>
|
||||
#include <xtils.hpp>
|
||||
#include <parser.hpp>
|
||||
|
||||
namespace vm {
|
||||
/// <summary>
|
||||
/// struct containing encoded data for a given virtual instruction...
|
||||
/// </summary>
|
||||
struct vinstr_data {
|
||||
/// <summary>
|
||||
/// vm handler index also known as the opcode...
|
||||
/// </summary>
|
||||
std::uint8_t vm_handler;
|
||||
|
||||
/// <summary>
|
||||
/// this field contains the second operand if any...
|
||||
/// </summary>
|
||||
std::uint64_t operand;
|
||||
|
||||
/// <summary>
|
||||
/// size in bits of the second operand if any... zero if none...
|
||||
/// </summary>
|
||||
std::uint8_t imm_size;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// struct containing all information for a label...
|
||||
/// </summary>
|
||||
struct vlabel_data {
|
||||
/// <summary>
|
||||
/// name of the label...
|
||||
/// </summary>
|
||||
std::string label_name;
|
||||
|
||||
/// <summary>
|
||||
/// vector of encoded virtual instructions...
|
||||
/// </summary>
|
||||
std::vector<vinstr_data> vinstrs;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// struct containing compiled virtual instructions (encoded and encrypted) for
|
||||
/// a given label...
|
||||
/// </summary>
|
||||
struct compiled_label_data {
|
||||
/// <summary>
|
||||
/// label name...
|
||||
/// </summary>
|
||||
std::string label_name;
|
||||
|
||||
/// <summary>
|
||||
/// relative virtual address from vm_entry to the virtual instructions...
|
||||
/// </summary>
|
||||
std::uintptr_t alloc_rva;
|
||||
|
||||
/// <summary>
|
||||
/// encrypted relative virtual address from vm_entry to virtual
|
||||
/// instructions...
|
||||
/// </summary>
|
||||
std::uintptr_t enc_alloc_rva;
|
||||
|
||||
/// <summary>
|
||||
/// vector of bytes containing the raw, encrypted virtual instructions...
|
||||
/// </summary>
|
||||
std::vector<std::uint8_t> vinstrs;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// class containing member functions used to encode and encrypted virtual
|
||||
/// instructions...
|
||||
/// </summary>
|
||||
class compiler_t {
|
||||
public:
|
||||
/// <summary>
|
||||
/// default constructor
|
||||
/// </summary>
|
||||
/// <param name="vmctx">pointer to a vm context object which has already been
|
||||
/// init...</param>
|
||||
explicit compiler_t(vm::ctx_t* vmctx);
|
||||
|
||||
/// <summary>
|
||||
/// encode virtual instructions from parser::virt_labels
|
||||
/// </summary>
|
||||
/// <returns>returns a vector of labels containing encoded virtual
|
||||
/// instructions</returns>
|
||||
std::vector<vlabel_data>* encode();
|
||||
|
||||
/// <summary>
|
||||
/// encrypt virtual instructions from parser::virt_labels
|
||||
/// </summary>
|
||||
/// <returns>returns a vector of compiled labels containing encoded and
|
||||
/// encrypted virtual instructions...</returns>
|
||||
std::vector<compiled_label_data> encrypt();
|
||||
|
||||
private:
|
||||
/// <summary>
|
||||
/// encrypt virtual instructions rva... <a
|
||||
/// href="https://back.engineering/17/05/2021/#vm_entry">read more here...</a>
|
||||
/// </summary>
|
||||
/// <param name="rva">relative virtual address to encrypted virtual
|
||||
/// instructions...</param> <returns></returns>
|
||||
std::uint64_t encrypt_rva(std::uint64_t rva);
|
||||
|
||||
/// <summary>
|
||||
/// pointer to the vmctx passed in by the constructor...
|
||||
/// </summary>
|
||||
vm::ctx_t* vmctx;
|
||||
|
||||
/// <summary>
|
||||
/// transformations used to decrypt the opcode operand extracted from
|
||||
/// calc_jmp... you can read more <a
|
||||
/// href="https://back.engineering/17/05/2021/#calc_jmp">here...</a>
|
||||
/// </summary>
|
||||
transform::map_t calc_jmp_transforms;
|
||||
|
||||
/// <summary>
|
||||
/// vector of encoded labels...
|
||||
/// </summary>
|
||||
std::vector<vlabel_data> virt_labels;
|
||||
|
||||
/// <summary>
|
||||
/// vector of decoded zydis instructions containing the native instructions to
|
||||
/// encrypt the virtual instruction rva which will be pushed onto the stack
|
||||
/// prior to jmping to vm entry...
|
||||
/// </summary>
|
||||
std::vector<zydis_decoded_instr_t> encrypt_vinstrs_rva;
|
||||
};
|
||||
} // namespace vm
|
@ -1,194 +1,211 @@
|
||||
#include "compiler.h"
|
||||
#include <compiler.hpp>
|
||||
|
||||
namespace vm
|
||||
{
|
||||
compiler_t::compiler_t( vm::ctx_t *vmctx ) : vmctx( vmctx )
|
||||
{
|
||||
if ( !parse_t::get_instance()->for_each( [ & ]( _vlabel_meta *label_data ) -> bool {
|
||||
std::printf( "> checking label %s for invalid instructions... number of instructions = %d\n",
|
||||
label_data->label_name.c_str(), label_data->vinstrs.size() );
|
||||
namespace vm {
|
||||
compiler_t::compiler_t(vm::ctx_t* vmctx) : vmctx(vmctx) {
|
||||
if (!parse_t::get_instance()->for_each([&](_vlabel_meta* label_data) -> bool {
|
||||
std::printf(
|
||||
"> checking label %s for invalid instructions... number of "
|
||||
"instructions = %d\n",
|
||||
label_data->label_name.c_str(), label_data->vinstrs.size());
|
||||
|
||||
const auto result = std::find_if(
|
||||
label_data->vinstrs.begin(), label_data->vinstrs.end(),
|
||||
[ & ]( const _vinstr_meta &vinstr ) -> bool {
|
||||
std::printf( "> vinstr name = %s, has imm = %d, imm = 0x%p\n", vinstr.name.c_str(),
|
||||
vinstr.has_imm, vinstr.imm );
|
||||
[&](const _vinstr_meta& vinstr) -> bool {
|
||||
std::printf("> vinstr name = %s, has imm = %d, imm = 0x%p\n",
|
||||
vinstr.name.c_str(), vinstr.has_imm, vinstr.imm);
|
||||
|
||||
for ( auto &vm_handler : vmctx->vm_handlers )
|
||||
if ( vm_handler.profile && vm_handler.profile->name == vinstr.name )
|
||||
for (auto& vm_handler : vmctx->vm_handlers)
|
||||
if (vm_handler.profile &&
|
||||
vm_handler.profile->name == vinstr.name)
|
||||
return false;
|
||||
|
||||
std::printf( "[!] this vm protected file does not have the vm handler for: %s...\n",
|
||||
vinstr.name.c_str() );
|
||||
std::printf(
|
||||
"[!] this vm protected file does not have the vm handler "
|
||||
"for: %s...\n",
|
||||
vinstr.name.c_str());
|
||||
|
||||
return true;
|
||||
} );
|
||||
});
|
||||
|
||||
return result == label_data->vinstrs.end();
|
||||
} ) )
|
||||
{
|
||||
std::printf( "[!] binary does not have the required vm handlers...\n" );
|
||||
exit( -1 );
|
||||
})) {
|
||||
std::printf("[!] binary does not have the required vm handlers...\n");
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
if ( !vm::handler::get_operand_transforms( vmctx->calc_jmp, calc_jmp_transforms ) )
|
||||
{
|
||||
std::printf( "[!] failed to extract calc_jmp transformations...\n" );
|
||||
exit( -1 );
|
||||
if (!vm::handler::get_operand_transforms(vmctx->calc_jmp,
|
||||
calc_jmp_transforms)) {
|
||||
std::printf("[!] failed to extract calc_jmp transformations...\n");
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
if ( !vm::instrs::get_rva_decrypt( vmctx->vm_entry, encrypt_vinstrs_rva ) )
|
||||
{
|
||||
std::printf( "[!] failed to extract virtual instruction rva decryption instructions...\n" );
|
||||
exit( -1 );
|
||||
if (!vm::instrs::get_rva_decrypt(vmctx->vm_entry, encrypt_vinstrs_rva)) {
|
||||
std::printf(
|
||||
"[!] failed to extract virtual instruction rva decryption "
|
||||
"instructions...\n");
|
||||
exit(-1);
|
||||
}
|
||||
|
||||
if ( !vm::transform::inverse_transforms( encrypt_vinstrs_rva ) )
|
||||
{
|
||||
std::printf( "[!] failed to inverse virtual instruction rva decrypt instructions...\n" );
|
||||
exit( -1 );
|
||||
}
|
||||
if (!vm::transform::inverse_transforms(encrypt_vinstrs_rva)) {
|
||||
std::printf(
|
||||
"[!] failed to inverse virtual instruction rva decrypt "
|
||||
"instructions...\n");
|
||||
exit(-1);
|
||||
}
|
||||
}
|
||||
|
||||
std::vector< vlabel_data > *compiler_t::encode()
|
||||
{
|
||||
parse_t::get_instance()->for_each( [ & ]( _vlabel_meta *label_data ) -> bool {
|
||||
virt_labels.push_back( { label_data->label_name } );
|
||||
for ( const auto &vinstr : label_data->vinstrs )
|
||||
{
|
||||
for ( auto idx = 0u; idx < 256; ++idx )
|
||||
{
|
||||
const auto &vm_handler = vmctx->vm_handlers[ idx ];
|
||||
if ( vm_handler.profile && !vinstr.name.compare( vm_handler.profile->name ) )
|
||||
{
|
||||
std::vector<vlabel_data>* compiler_t::encode() {
|
||||
parse_t::get_instance()->for_each([&](_vlabel_meta* label_data) -> bool {
|
||||
virt_labels.push_back({label_data->label_name});
|
||||
for (const auto& vinstr : label_data->vinstrs) {
|
||||
for (auto idx = 0u; idx < 256; ++idx) {
|
||||
const auto& vm_handler = vmctx->vm_handlers[idx];
|
||||
if (vm_handler.profile &&
|
||||
!vinstr.name.compare(vm_handler.profile->name)) {
|
||||
virt_labels.back().vinstrs.push_back(
|
||||
{ ( std::uint8_t )idx, vinstr.imm, vm_handler.profile->imm_size } );
|
||||
{(std::uint8_t)idx, vinstr.imm, vm_handler.profile->imm_size});
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
} );
|
||||
});
|
||||
|
||||
return &virt_labels;
|
||||
}
|
||||
}
|
||||
|
||||
std::vector< compiled_label_data > compiler_t::encrypt()
|
||||
{
|
||||
std::vector< compiled_label_data > result;
|
||||
std::vector<compiled_label_data> compiler_t::encrypt() {
|
||||
std::vector<compiled_label_data> result;
|
||||
const auto end_of_module = vmctx->image_size + vmctx->image_base;
|
||||
|
||||
// decryption key starts off as the image
|
||||
// base address of the virtual instructions...
|
||||
std::uintptr_t decrypt_key = end_of_module, start_addr;
|
||||
if ( vmctx->exec_type == vmp2::exec_type_t::backward )
|
||||
std::for_each( virt_labels.begin()->vinstrs.begin(), virt_labels.begin()->vinstrs.end(),
|
||||
[ & ]( const vinstr_data &vinstr ) {
|
||||
( ++decrypt_key ) += vinstr.imm_size ? vinstr.imm_size / 8 : 0;
|
||||
} );
|
||||
if (vmctx->exec_type == vmp2::exec_type_t::backward)
|
||||
std::for_each(
|
||||
virt_labels.begin()->vinstrs.begin(),
|
||||
virt_labels.begin()->vinstrs.end(), [&](const vinstr_data& vinstr) {
|
||||
(++decrypt_key) += vinstr.imm_size ? vinstr.imm_size / 8 : 0;
|
||||
});
|
||||
|
||||
const auto opcode_fetch = std::find_if(
|
||||
vmctx->calc_jmp.begin(), vmctx->calc_jmp.end(), []( const zydis_instr_t &instr_data ) -> bool {
|
||||
vmctx->calc_jmp.begin(), vmctx->calc_jmp.end(),
|
||||
[](const zydis_instr_t& instr_data) -> bool {
|
||||
// mov/movsx/movzx rax/eax/ax/al, [rsi]
|
||||
return instr_data.instr.operand_count > 1 &&
|
||||
( instr_data.instr.mnemonic == ZYDIS_MNEMONIC_MOV ||
|
||||
(instr_data.instr.mnemonic == ZYDIS_MNEMONIC_MOV ||
|
||||
instr_data.instr.mnemonic == ZYDIS_MNEMONIC_MOVSX ||
|
||||
instr_data.instr.mnemonic == ZYDIS_MNEMONIC_MOVZX ) &&
|
||||
instr_data.instr.operands[ 0 ].type == ZYDIS_OPERAND_TYPE_REGISTER &&
|
||||
util::reg::to64( instr_data.instr.operands[ 0 ].reg.value ) == ZYDIS_REGISTER_RAX &&
|
||||
instr_data.instr.operands[ 1 ].type == ZYDIS_OPERAND_TYPE_MEMORY &&
|
||||
instr_data.instr.operands[ 1 ].mem.base == ZYDIS_REGISTER_RSI;
|
||||
} );
|
||||
|
||||
if ( opcode_fetch == vmctx->calc_jmp.end() )
|
||||
{
|
||||
std::printf( "> critical error trying to find opcode fetch inside of compiler_t::encrypt...\n" );
|
||||
exit( 0 );
|
||||
instr_data.instr.mnemonic == ZYDIS_MNEMONIC_MOVZX) &&
|
||||
instr_data.instr.operands[0].type ==
|
||||
ZYDIS_OPERAND_TYPE_REGISTER &&
|
||||
util::reg::to64(instr_data.instr.operands[0].reg.value) ==
|
||||
ZYDIS_REGISTER_RAX &&
|
||||
instr_data.instr.operands[1].type == ZYDIS_OPERAND_TYPE_MEMORY &&
|
||||
instr_data.instr.operands[1].mem.base == ZYDIS_REGISTER_RSI;
|
||||
});
|
||||
|
||||
if (opcode_fetch == vmctx->calc_jmp.end()) {
|
||||
std::printf(
|
||||
"> critical error trying to find opcode fetch inside of "
|
||||
"compiler_t::encrypt...\n");
|
||||
exit(0);
|
||||
}
|
||||
|
||||
start_addr = decrypt_key - 1; // make it zero based...
|
||||
std::for_each( virt_labels.begin(), virt_labels.end(), [ & ]( vm::vlabel_data &label ) {
|
||||
std::for_each(
|
||||
virt_labels.begin(), virt_labels.end(), [&](vm::vlabel_data& label) {
|
||||
// sometimes there is a mov al, [rsi-1]... we want that disp...
|
||||
if ( opcode_fetch->instr.operands[ 1 ].mem.disp.has_displacement )
|
||||
start_addr += std::abs( opcode_fetch->instr.operands[ 1 ].mem.disp.value );
|
||||
if (opcode_fetch->instr.operands[1].mem.disp.has_displacement)
|
||||
start_addr +=
|
||||
std::abs(opcode_fetch->instr.operands[1].mem.disp.value);
|
||||
|
||||
decrypt_key = start_addr;
|
||||
result.push_back( { label.label_name, start_addr } );
|
||||
result.push_back({label.label_name, start_addr});
|
||||
|
||||
if ( vmctx->exec_type == vmp2::exec_type_t::forward )
|
||||
{
|
||||
std::for_each( label.vinstrs.begin(), label.vinstrs.end(), [ & ]( vm::vinstr_data &vinstr ) {
|
||||
if (vmctx->exec_type == vmp2::exec_type_t::forward) {
|
||||
std::for_each(
|
||||
label.vinstrs.begin(), label.vinstrs.end(),
|
||||
[&](vm::vinstr_data& vinstr) {
|
||||
std::uint8_t opcode = vinstr.vm_handler;
|
||||
std::uint64_t operand = 0u;
|
||||
|
||||
// encrypt opcode...
|
||||
std::tie( opcode, decrypt_key ) =
|
||||
vm::instrs::encrypt_operand( calc_jmp_transforms, vinstr.vm_handler, decrypt_key );
|
||||
std::tie(opcode, decrypt_key) = vm::instrs::encrypt_operand(
|
||||
calc_jmp_transforms, vinstr.vm_handler, decrypt_key);
|
||||
|
||||
// if there is an operand then we will encrypt that as well..
|
||||
if ( vmctx->vm_handlers[ vinstr.vm_handler ].imm_size )
|
||||
{
|
||||
auto &vm_handler_transforms = vmctx->vm_handlers[ vinstr.vm_handler ].transforms;
|
||||
std::tie( operand, decrypt_key ) =
|
||||
vm::instrs::encrypt_operand( vm_handler_transforms, vinstr.operand, decrypt_key );
|
||||
}
|
||||
else // else just push back the opcode...
|
||||
{
|
||||
result.back().vinstrs.push_back( opcode );
|
||||
if (vmctx->vm_handlers[vinstr.vm_handler].imm_size) {
|
||||
auto& vm_handler_transforms =
|
||||
vmctx->vm_handlers[vinstr.vm_handler].transforms;
|
||||
std::tie(operand, decrypt_key) = vm::instrs::encrypt_operand(
|
||||
vm_handler_transforms, vinstr.operand, decrypt_key);
|
||||
} else // else just push back the opcode...
|
||||
{
|
||||
result.back().vinstrs.push_back(opcode);
|
||||
return; // finished here...
|
||||
}
|
||||
|
||||
result.back().vinstrs.push_back( opcode );
|
||||
for ( auto idx = 0u; idx < vmctx->vm_handlers[ vinstr.vm_handler ].imm_size / 8; ++idx )
|
||||
result.back().vinstrs.push_back( reinterpret_cast< std::uint8_t * >( &vinstr.operand )[ idx ] );
|
||||
} );
|
||||
}
|
||||
else
|
||||
{
|
||||
std::for_each( label.vinstrs.begin(), label.vinstrs.end(), [ & ]( vm::vinstr_data &vinstr ) {
|
||||
result.back().vinstrs.push_back(opcode);
|
||||
for (auto idx = 0u;
|
||||
idx < vmctx->vm_handlers[vinstr.vm_handler].imm_size / 8;
|
||||
++idx)
|
||||
result.back().vinstrs.push_back(
|
||||
reinterpret_cast<std::uint8_t*>(&vinstr.operand)[idx]);
|
||||
});
|
||||
} else {
|
||||
std::for_each(
|
||||
label.vinstrs.begin(), label.vinstrs.end(),
|
||||
[&](vm::vinstr_data& vinstr) {
|
||||
std::uint8_t opcode = vinstr.vm_handler;
|
||||
std::uint64_t operand = 0u;
|
||||
|
||||
// encrypt opcode...
|
||||
std::tie( opcode, decrypt_key ) =
|
||||
vm::instrs::encrypt_operand( calc_jmp_transforms, vinstr.vm_handler, decrypt_key );
|
||||
std::tie(opcode, decrypt_key) = vm::instrs::encrypt_operand(
|
||||
calc_jmp_transforms, vinstr.vm_handler, decrypt_key);
|
||||
|
||||
// if there is an operand then we will encrypt that as well..
|
||||
if ( vmctx->vm_handlers[ vinstr.vm_handler ].imm_size )
|
||||
{
|
||||
auto &vm_handler_transforms = vmctx->vm_handlers[ vinstr.vm_handler ].transforms;
|
||||
std::tie( operand, decrypt_key ) =
|
||||
vm::instrs::encrypt_operand( vm_handler_transforms, vinstr.operand, decrypt_key );
|
||||
}
|
||||
else // else just push back the opcode...
|
||||
{
|
||||
result.back().vinstrs.insert( result.back().vinstrs.begin(), 1, opcode );
|
||||
if (vmctx->vm_handlers[vinstr.vm_handler].imm_size) {
|
||||
auto& vm_handler_transforms =
|
||||
vmctx->vm_handlers[vinstr.vm_handler].transforms;
|
||||
std::tie(operand, decrypt_key) = vm::instrs::encrypt_operand(
|
||||
vm_handler_transforms, vinstr.operand, decrypt_key);
|
||||
} else // else just push back the opcode...
|
||||
{
|
||||
result.back().vinstrs.insert(result.back().vinstrs.begin(), 1,
|
||||
opcode);
|
||||
return; // finished here...
|
||||
}
|
||||
|
||||
// operand goes first, then opcode when vip advances backwards...
|
||||
std::vector< std::uint8_t > _temp;
|
||||
for ( auto idx = 0u; idx < vmctx->vm_handlers[ vinstr.vm_handler ].imm_size / 8; ++idx )
|
||||
_temp.push_back( reinterpret_cast< std::uint8_t * >( &operand )[ idx ] );
|
||||
// operand goes first, then opcode when vip advances
|
||||
// backwards...
|
||||
std::vector<std::uint8_t> _temp;
|
||||
for (auto idx = 0u;
|
||||
idx < vmctx->vm_handlers[vinstr.vm_handler].imm_size / 8;
|
||||
++idx)
|
||||
_temp.push_back(
|
||||
reinterpret_cast<std::uint8_t*>(&operand)[idx]);
|
||||
|
||||
result.back().vinstrs.insert( result.back().vinstrs.begin(), _temp.begin(), _temp.end() );
|
||||
result.back().vinstrs.insert( result.back().vinstrs.begin() + _temp.size(), opcode );
|
||||
} );
|
||||
result.back().vinstrs.insert(result.back().vinstrs.begin(),
|
||||
_temp.begin(), _temp.end());
|
||||
result.back().vinstrs.insert(
|
||||
result.back().vinstrs.begin() + _temp.size(), opcode);
|
||||
});
|
||||
}
|
||||
|
||||
result.back().enc_alloc_rva = encrypt_rva( start_addr );
|
||||
start_addr += result.back().vinstrs.size() - 1; // make it zero based...
|
||||
} );
|
||||
result.back().enc_alloc_rva = encrypt_rva(start_addr);
|
||||
start_addr +=
|
||||
result.back().vinstrs.size() - 1; // make it zero based...
|
||||
});
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
std::uint64_t compiler_t::encrypt_rva( std::uint64_t rva )
|
||||
{
|
||||
for ( auto &instr : encrypt_vinstrs_rva )
|
||||
rva = vm::transform::apply( instr.operands[ 0 ].size, instr.mnemonic, rva,
|
||||
transform::has_imm( &instr ) ? instr.operands[ 1 ].imm.value.u : 0 );
|
||||
std::uint64_t compiler_t::encrypt_rva(std::uint64_t rva) {
|
||||
for (auto& instr : encrypt_vinstrs_rva)
|
||||
rva = vm::transform::apply(
|
||||
instr.operands[0].size, instr.mnemonic, rva,
|
||||
transform::has_imm(&instr) ? instr.operands[1].imm.value.u : 0);
|
||||
|
||||
return rva;
|
||||
}
|
||||
}
|
||||
} // namespace vm
|
@ -1,130 +0,0 @@
|
||||
#pragma once
|
||||
#include <Windows.h>
|
||||
#include <algorithm>
|
||||
#include <vmprofiler.hpp>
|
||||
#include <xtils.hpp>
|
||||
|
||||
#include "parser.h"
|
||||
|
||||
namespace vm
|
||||
{
|
||||
/// <summary>
|
||||
/// struct containing encoded data for a given virtual instruction...
|
||||
/// </summary>
|
||||
struct vinstr_data
|
||||
{
|
||||
/// <summary>
|
||||
/// vm handler index also known as the opcode...
|
||||
/// </summary>
|
||||
std::uint8_t vm_handler;
|
||||
|
||||
/// <summary>
|
||||
/// this field contains the second operand if any...
|
||||
/// </summary>
|
||||
std::uint64_t operand;
|
||||
|
||||
/// <summary>
|
||||
/// size in bits of the second operand if any... zero if none...
|
||||
/// </summary>
|
||||
std::uint8_t imm_size;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// struct containing all information for a label...
|
||||
/// </summary>
|
||||
struct vlabel_data
|
||||
{
|
||||
/// <summary>
|
||||
/// name of the label...
|
||||
/// </summary>
|
||||
std::string label_name;
|
||||
|
||||
/// <summary>
|
||||
/// vector of encoded virtual instructions...
|
||||
/// </summary>
|
||||
std::vector< vinstr_data > vinstrs;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// struct containing compiled virtual instructions (encoded and encrypted) for a given label...
|
||||
/// </summary>
|
||||
struct compiled_label_data
|
||||
{
|
||||
/// <summary>
|
||||
/// label name...
|
||||
/// </summary>
|
||||
std::string label_name;
|
||||
|
||||
/// <summary>
|
||||
/// relative virtual address from vm_entry to the virtual instructions...
|
||||
/// </summary>
|
||||
std::uintptr_t alloc_rva;
|
||||
|
||||
/// <summary>
|
||||
/// encrypted relative virtual address from vm_entry to virtual instructions...
|
||||
/// </summary>
|
||||
std::uintptr_t enc_alloc_rva;
|
||||
|
||||
/// <summary>
|
||||
/// vector of bytes containing the raw, encrypted virtual instructions...
|
||||
/// </summary>
|
||||
std::vector< std::uint8_t > vinstrs;
|
||||
};
|
||||
|
||||
/// <summary>
|
||||
/// class containing member functions used to encode and encrypted virtual instructions...
|
||||
/// </summary>
|
||||
class compiler_t
|
||||
{
|
||||
public:
|
||||
/// <summary>
|
||||
/// default constructor
|
||||
/// </summary>
|
||||
/// <param name="vmctx">pointer to a vm context object which has already been init...</param>
|
||||
explicit compiler_t( vm::ctx_t *vmctx );
|
||||
|
||||
/// <summary>
|
||||
/// encode virtual instructions from parser::virt_labels
|
||||
/// </summary>
|
||||
/// <returns>returns a vector of labels containing encoded virtual instructions</returns>
|
||||
std::vector< vlabel_data > *encode();
|
||||
|
||||
/// <summary>
|
||||
/// encrypt virtual instructions from parser::virt_labels
|
||||
/// </summary>
|
||||
/// <returns>returns a vector of compiled labels containing encoded and encrypted virtual
|
||||
/// instructions...</returns>
|
||||
std::vector< compiled_label_data > encrypt();
|
||||
|
||||
private:
|
||||
/// <summary>
|
||||
/// encrypt virtual instructions rva... <a href="https://back.engineering/17/05/2021/#vm_entry">read more
|
||||
/// here...</a>
|
||||
/// </summary>
|
||||
/// <param name="rva">relative virtual address to encrypted virtual instructions...</param>
|
||||
/// <returns></returns>
|
||||
std::uint64_t encrypt_rva( std::uint64_t rva );
|
||||
|
||||
/// <summary>
|
||||
/// pointer to the vmctx passed in by the constructor...
|
||||
/// </summary>
|
||||
vm::ctx_t *vmctx;
|
||||
|
||||
/// <summary>
|
||||
/// transformations used to decrypt the opcode operand extracted from calc_jmp...
|
||||
/// you can read more <a href="https://back.engineering/17/05/2021/#calc_jmp">here...</a>
|
||||
/// </summary>
|
||||
transform::map_t calc_jmp_transforms;
|
||||
|
||||
/// <summary>
|
||||
/// vector of encoded labels...
|
||||
/// </summary>
|
||||
std::vector< vlabel_data > virt_labels;
|
||||
|
||||
/// <summary>
|
||||
/// vector of decoded zydis instructions containing the native instructions to encrypt the virtual instruction
|
||||
/// rva which will be pushed onto the stack prior to jmping to vm entry...
|
||||
/// </summary>
|
||||
std::vector< zydis_decoded_instr_t > encrypt_vinstrs_rva;
|
||||
};
|
||||
} // namespace vm
|
@ -1,36 +1,29 @@
|
||||
#include "parser.h"
|
||||
#include <parser.hpp>
|
||||
|
||||
parse_t::parse_t()
|
||||
{
|
||||
}
|
||||
parse_t::parse_t() {}
|
||||
|
||||
auto parse_t::get_instance() -> parse_t *
|
||||
{
|
||||
auto parse_t::get_instance() -> parse_t* {
|
||||
static parse_t obj;
|
||||
return &obj;
|
||||
}
|
||||
|
||||
void parse_t::add_label( std::string label_name )
|
||||
{
|
||||
void parse_t::add_label(std::string label_name) {
|
||||
// remove ":" from the end of the label name...
|
||||
label_name.erase( label_name.end() - 1 );
|
||||
virt_labels.push_back( { label_name } );
|
||||
label_name.erase(label_name.end() - 1);
|
||||
virt_labels.push_back({label_name});
|
||||
}
|
||||
|
||||
void parse_t::add_vinstr( std::string vinstr_name )
|
||||
{
|
||||
virt_labels.back().vinstrs.push_back( { vinstr_name, false, 0u } );
|
||||
void parse_t::add_vinstr(std::string vinstr_name) {
|
||||
virt_labels.back().vinstrs.push_back({vinstr_name, false, 0u});
|
||||
}
|
||||
|
||||
void parse_t::add_vinstr( std::string vinstr_name, std::uintptr_t imm_val )
|
||||
{
|
||||
virt_labels.back().vinstrs.push_back( { vinstr_name, true, imm_val } );
|
||||
void parse_t::add_vinstr(std::string vinstr_name, std::uintptr_t imm_val) {
|
||||
virt_labels.back().vinstrs.push_back({vinstr_name, true, imm_val});
|
||||
}
|
||||
|
||||
bool parse_t::for_each( callback_t callback )
|
||||
{
|
||||
for ( auto &entry : virt_labels )
|
||||
if ( !callback( &entry ) )
|
||||
bool parse_t::for_each(callback_t callback) {
|
||||
for (auto& entry : virt_labels)
|
||||
if (!callback(&entry))
|
||||
return false;
|
||||
|
||||
return true;
|
||||
|
Loading…
Reference in new issue