20 minutes read:
Building the Malware sample
The Process of Hiding Suspicious Indicators from Malware Analysts
An In-Depth Analysis of The PE File Sections: Focusing on the Import section
Bootstrapping based AV evasion technique
Building the Malware sample:
As you may saw in the sample code,I called 3 external functions, 3 functions that are not part of no common libraries but my own personal library.. Without it, The linker won't be able to output the final executable or simply the sample program won't be able to do what is supposed to do, And add more trouble to that, these external funcs themselves relay on totally external libraries sometimes, so the first logical step to do is go and figure out the dependencies of my personal library and by looking at the include headers luckily we only relay on 1 external/3rd_party lib which is sqlite3, A single header amalgamation file that we can link statically against our main object file meaning our external lib will be self sufficient when it comes to resolving the dependencies at runtime. as i made it clear in the first article you need to have certain knowledge to continue through with this so you don't get frustrated. This Demonstration below shouldn't be taken as literal step by step guide since it's really easy to rebuild yourself,
your folder structure should be always organized with at least 5 sub directories like this, Every file should be well organized
Sample folder {
// naming things is up to you.
src_code -> main.c, external_lib.c
header_files -> external_lib_header.h, sqlite3.h
bin -> sample.exe
libraries // Not needed
imports // Not needed
Makefileb
}
Dependencies:
- download the sqlite3 [amalgamation version]
- extract zip
- Copy sqlite3.h into Sample/header_files/sqlite3.c then Sample/header_files/
- cd into Sample/src_code then use your preferd coding editor to open sampel.c with.
- manually change evrey instance of the func s() with what the safe version of strcat(This a small c excerise on it's own because im tight on time to do it)
- save and exit
- inside Sample run gcc(mingw version) -shared -o bin/libsample.dll -Iheader_files/ src_code/external_lib.c header_files/sqlite3.h -Wl,--out-implib,imports/libsample.dll.a
- gcc -o bin/sample.exe -Iheader_files src_code/main.c -Limports -lsample
Detect it easy & Virus total:
The second post got automatically refused on /d/malware due to the sub's regulations on sharing links. Unfortunately, this really put a wrench in my plans for this second part, as I physically won't be able to go through every detail I wanted to talk about, since sharing screenshots was my only means of grabbing attention. But no worries, I will still power through and write any information I think is interesting by text.
Starting with D.I.E., this tool showed no detection/s, but it listed the imported libraries needed for the program to run, and this is the bread and butter of the later sections.
VirusTotal most notably showed random hits from two obscure AV vendors without any correct guess on what the sample really is or does. I also waited for the dynamic analysis. It showed nothing worthy to write about and windows defender on win11 and not 10 mysteriously allowed it to run once on hen quarantined it. Overall, the things worth mentioning from VirusTotal are the imported libraries' DLl names, imported function names, and the scattered symbol strings/whatever they may be or of any value for malware analysis, since analysts use VirusTotal to look for known patterns or behaviors to catch malware. So, it's always good to write dead code in your malware's payload. Dead code is just dead weight unused or irrelevant code snippets. this confuses analysts as they struggle to distinguish between important and redundant code. Obfuscation is all about making code look confusing and messy on purpose.
The Process of Hiding Suspicious Indicators from Malware Analysts:
Have you ever wondered about the mechanism of VirusTotal after uploading a file and watching it generate a detailed report in seconds? For me, the imports always stood out the most. For example, having too few imports is very suspicious, and having all your imports showing is like having all your cards laid out flat on the table.
That's when I learned how big of a deal it was to hide my naked imports, since they essentially expose my true intentions to any malware analyst. They reveal the capabilities of the program just from looking at the outside. That realization is what pulled me into the rabbit hole of malware obfuscation and cryptography in general. I would love to explain all the other information found in a VirusTotal report with the same depth I'm about to give the import section, But I'm simply worried i i won't fit the character limits on here and not being able to share any screenshots also really sucks.
So, quick tips when coding, always try to innovate and write your own functions (like I did in the external library) and statically link them. However, even after doing that, they will still end up as character strings in the .rdata section. So again, instead of writing clean, easy-to-read code, try to scramble it with weird variable names, unnecessary steps, and twisted logic or even better only include encrypted strings in your payload and have them decrypted at run time.
To a human (or an antivirus), it should look like a tangled mess.But underneath you must always make sure the code still works just fine.
As I mentioned in the first part of the article, antivirus software often relies on signature-based detection; it maintains a database of known malicious code patterns.
These signatures frequently include specific function and variable names commonly used in malware, SO don't expect reusing malware from 2016 without any preparation.
For example, if malware frequently uses function names like inject_payload() or variables like shellcode, these become part of the signature.
Here’s why renaming is surprisingly effective:
- String Matching: VirusTotal's heuristic scans, for example, look for exact matches of known malicious strings. By simply renaming functions, the code’s functionality remains identical,
- but the signature is completely different.
// Original malicious code
void inject_payload() {
char* shellcode = "...";
}
// After renaming
void _IlO1abc() {
char* _x1y2 = "...";
}
An In-Depth Analysis of The PE File Sections: Focusing on the Import section
Import Directory Quick Reference
Import Directory Table
Single directory entry inside the PE Optional Header’s Data Directory array.
IMAGE_IMPORT_DESCRIPTOR array
An array of structs 20 bytes each, One struct per Imported DLL and ends with a 20 byte zero filled space marking the end for the imported dlls .
IMAGE_THUNK_DATA (INT & IAT)
An array of structs(thunks) (one per imported function), Ends with a zero thunk (8 zero bytes), This struct is referenced by OFT (OriginalFirstThunk) and FT (FirstThunk) 2 members of of the IMAGE_IMPORT_DESCRIPTOR and mirror each other until the loader resolves imports and overwrites FT with the imported func addresses
IMAGE_IMPORT_BY_NAME
A struct referenced by the IMAGE_THUNK_DATA, Basically A variable-length struct containing the actual name of the imported function.
With that out of the way, and since I mentioned imports earlier, let's work towards hiding them. I'm not entirely sure about all the inner workings of these online scanning tools that process malware samples, but I am familiar with the PE/COFF formats and how they are structured on disk versus at runtime in memory, so I can make an educated guess.
The process of parsing imports most definitely begins by looking at the Import Directory Table. For example:
- If VirusTotal processes imports at runtime only, they would refer to the IMAGE_THUNK_DATA returned by the IAT (Import Address Table), which will have an absolute pointer to the function code inside the loaded imported DLL. The DLL’s base address plus the IAT thunk will give you the function name.
- If VirusTotal processes imports statically (on disk), then the process will be a loop of pointer referencing: IMAGE_IMPORT_DESCRIPTOR[N].OriginalFirstThunk -> THUNK_DATA -> IMAGE_IMPORT_BY_NAME char pointer, Very simple.
With this new knowledge acquired about this section, we come to the realization that to hide the imports, you would need to mess them up in a way. But if you encrypt them for example , you would need to make some sort of a bootstrap program first. This made me realize the concept of custom loading, packing, and bootstrapping.
Bootstrapping based AV evasion technique:
Have you seen terms like UPX, loaders, and packers get thrown around often here but could never comprehend their whole purpose?
They are tools combined with your payload to leap over the initial point of intrusion that's exactly what they're for. This method is better than dropping a fresh executable payload and creating a new process, which is very likely to get you flagged during an engagement.
After understanding their purpose and personally using them, I was left with a hard question. I was struggling to understand how UPX reconstructs the PE imports, And the exe in general , since I thought the Windows loader was the responsible party for configuring them. After a long period of researching, I learned that UPX compresses yhe payload executable into one section and then uses another section a "stub" to reconstruct the payload into a fully functional PE image from the compressed blob.
The stub uses its own simple imports (LoadLibrary, GetProcAddress, VirtualAlloc) to perform the complex task of reconstructing the original program's IAT and environment. I really admired the engineering after i learned this concept. For example, rather than relying on the system loader to resolve the imports, your stub calls LoadLibraryA and GetProcAddress using the data referenced from the IDT, Like the Name variable from the IMAGE_IMPORT_DESCRIPTOR[N] struct to get the DLL name for LoadLibraryA(), and the IMPORT_BY_NAME variable to pull the imported function name for GetProcAddress. Those DLLs then are loaded into the stub's process memory space, so the imports are resolved before jumping to the "main function" of the payload.
The stub isn't just decompressing dataas I said, it's reconstructing a fully functional, executable, minimalist PE image from the compressed blob. In order to have this minimal image, i had tried to copy upx's phylosphy with including only the minimal data in order construct the image in memory By
A.Parsing the Compressed PE Headers: The compressed data isn't just the code. UPX for example stores a modified (often stripped) version of the original PE headers and section headers that contain only the bare minimum.
struct MinPEInfo {
DWORD EntryPointRVA;
DWORD ImageBase;
DWORD SizeOfImage;
DWORD SectionAlignment;
DWORD FileAlignment;
WORD NumberOfSections;
WORD Subsystem;
DWORD SizeOfHeaders;
};
struct MinSection {
DWORD VirtualAddress; // RVA where section loads
DWORD SizeOfRawData; // Size in file
DWORD VirtualSize; // Size in memory
DWORD Characteristics; // Permissions (rx/rw/etc)
DWORD PointerToRawData;
// Don't need to save the section names, names do not matter neither for us or the ldr
};
[list]
struct MinImports {
DWORD IAT_RVA; // Where IAT should be
DWORD NumDLLs; // How many DLLs
// Followed by: <DLL_name><func1><func2>...<0><next_DLL>...
};
B. Allocating Memory: The stub uses VirtualAlloc to allocate memory at the preferred base address of the original PE file, not the stub's own base address. If that allocation fails (because the memory is already occupied), it handles relocation by adjusting memory addresses accordingly.
C. Mapping Sections: It decompresses and copies each section (e.g., .text, .data, .idata, .rdata, .reloc, .pdata, etc.) from the compressed blob into the correct memory addresses within the newly allocated memory block, example
for (int i = 0; i < numSections; i++) {
BYTE* sectionDestination = imageBase + sections.VirtualAddress;
// Copy section data from your compressed or decrypted buffer
memcpy(sectionDestination, compressedData + sections[i].PointerToRawData,sections[i].SizeOfRawData);
// make sure section aligment isn't messed up
DWORD alignedVirtualSize = ALIGN_UP(sections[i].VirtualSize, SectionAlignment);
DWORD paddingSize = alignedVirtualSize - sections[i].SizeOfRawData;
if (paddingSize > 0) {
memset(sectionDestination + sections[i].SizeOfRawData, 0, paddingSize);
}
}
D. setting the correct page permissions (e.g., PAGE_EXECUTE_READ for .text and etc) using VirtualProtect later.
E. Fixing the Imports : It's as easy as delay loading.
F. Applying Base Relocations (if necessary): If the PE couldn't be loaded at its preferred base address(VirtualAlloc failed and returned NULL), the stub must manually apply all the fixups in the .reloc section, just like the OS loader would.
G. Registering exception handlers (OPTIONAL)
H. Transferring control to the payload's entry point (ImageBase + EntryPointRVA) should be simple. I forgot the exact implementation here since I just copied the needed code from somewhere back then and kept prompting ChatGPT until I got it working (it was in Assembly for NASM). However, I found an article that explains all these steps in C, with code at the end. It's called Writing a local PE Loader from scratch (for educational purposes)on Medium. Alternatively, you can just look at the source code in src/stub/src/amd64-win64.pe.
- I hope you learned anything new from reading this article. I'll be honest, I'm not entirely happy with how this final part came together. I originally planned it like a YouTube video, and it's obviously far from that since I can't share diagrams or visuals to explain any topic in depth in the heat of the moment, I'll adapt. However, the process pushed me to relearn this topic for myself, which was a valuable experience.
- If you have any questions, I'd be happy to answer them.Otherwise, until next time!
- fentanyl ദ്ദി(˵ •̀ ᴗ - ˵ ✧): 83WHKja96ab8ZfdtqosJDMds2nKnnmVxMfbX1KaZxgBd6petzX8WmTnUy2p1c2sKGcbnm9sn1dYKtAvAhmq4UqvSAQf6wpt
[i]