[Article] Random malware analysis & unpacking - Stage 2/3
<- Random Malware Analysis & Unpacking - Stage 1/3
Note : The first thread started by create a queue timer that was creating thread repeatedly. To avoid creating a queuetimer and exiting main thread (and exiting the whole program by the way), the main thread was waiting using WaitForSingleObject
that made it wait for 14mn by default. In order to examine further into our binary, I will patch the value passed to this function to be comfortable.
We now are jumping to a function somewhere that has been dynamically allocated, thus we wont be able to set breakpoint that will last on this section. This function is resolving a lot of function dynamically using a method similar to the method used in the main thread to resolve its function names.
The function resolves really specific functions you can check on the screenshot just below.
Those functions are pretty interesting. The presence of :
- NtUnmapViewOfSection
- functions relative to thread handling/creation
- functions relative to read and write memory
- functions relative to process creation
leads us to understand that the function is going to do a create replacement.
Here is the pseudo code of a classic process replacement (that matches exactly what is happening) in Practical Malware Analysis - a must have - :
CreateProcess(..., "procname", ..., CREATE_SUSPENDED, ...);
ZwUnmapViewOfSection(...);
VirtualAllocEx(..., ImageBase, SizeOfImage, ...);
WirteProcessMemory(..., headers, ...);
for (i = 0; i < NumberOfSections; i++){
WirteProcessMemory(..., section, ...);
}
SetThreadContext();
...
ResumeThread();
Here are the lines the program goes trough :
debug029:0047040C call eax ; CreateProcessW
[...]
debug029:00470431 call ecx ; GetThreadContext
[...]
debug029:00470457 call eax ; ReadProcessMemory
[...]
debug029:00470467 push eax ; baseAddress
debug029:00470468 mov ecx, [ebp+var_28]
debug029:0047046B push ecx ; handler
debug029:0047046C mov edx, [ebp+arg_0]
debug029:0047046F mov eax, [edx+38h]
debug029:00470472 call eax ; NtUnmapViewOfSection
[...]
debug029:00430493 call ecx ; VirtualAllocEx
[...]
debug029:004304BD call edx ; WriteProcessMemory
[...]
debug029:004304D1 loop_sections: ; CODE XREF: sub_430360+166↑j
debug029:004304D1 mov ecx, [ebp+ptr_PE_base]
debug029:004304D4 movzx edx, word ptr [ecx+6] ; [1]
debug029:004304D8 cmp [ebp+i], edx
debug029:004304DB jge short end_of_loop
[...]
debug029:0043051B call eax ; WriteProcessMemory
debug029:0043051F end_of_loop:
[...]
debug029:0043053E call edx ; WriteProcessMemory
[...]
debug029:0043056F call ecx ; SetThreadContext
[...]
debug029:0043057B call ecx ; ResumeThread
Those series of calls looks very similar to the case described in Practical Malware Analysis.
Note_[1] -> Virgilius project defines the 32 bits header as :
struct _IMAGE_NT_HEADERS
{
ULONG Signature; //0x0
struct _IMAGE_FILE_HEADER FileHeader; //0x4
struct _IMAGE_OPTIONAL_HEADER OptionalHeader; //0x18
};
struct _IMAGE_FILE_HEADER
{
USHORT Machine; //0x0
USHORT NumberOfSections; //0x2
ULONG TimeDateStamp; //0x4
ULONG PointerToSymbolTable; //0x8
ULONG NumberOfSymbols; //0xc
USHORT SizeOfOptionalHeader; //0x10
USHORT Characteristics; //0x12
};
So Signature is 4 bytes, the machine is 2 bytes. We can conclude that ecx+6
(at 0x004304D4) actually points to the NumberOfSections. So we are looping trough sections.
Now I will just use x32dbg to dump every section copied to the process using WriteProcessMemory
, reconstruct it and move on to the new PE.
Note : Doing so might be painful and prone to error, plus we aren’t sure that PE header will be correct (entry point, etc). For now I will just dump the buffer from which the bytes are copied in WriteProcessMemory, using x32dbg section dump utility to dump the array containing the PE.
We can now open our newly created PE.
First a quick look at PE-Bear :
From this graphic I can assume that there will be a third step to this binary, and this binary will probably be unpacking the next step and jumping to it.
Here is what the main looks like :
sub 0x401000
This function looks quite similar to what he have seen in the first binary: it probably resolves tons of imports. Plus, we can notice some encoded strings.
As I don’t want to solve all this manualy, I will use a script to create a big structure as in the first stage, and rename fields accordingly with the resolves.
def get_string(addr):
out = ""
while True:
if Byte(addr) != 0:
out += chr(Byte(addr))
else:
break
addr += 1
return out
id = AddStrucEx(-1, "R", 0);
for i in xrange(0,480,4):
AddStrucMember(id, "field_%x"%i, i, FF_DWRD, -1, 4);
#Memory Reg [Base Reg + Index Reg + Displacement]
MEM_REG = 4
STRUCT_NAME = "R"
function_name = ""
decoded = False
MakeNameEx(0xF91E0A, 'decodeString', SN_NOWARN);
Wait();
r = idaapi.get_struc(get_struc_id(STRUCT_NAME))
while True:
line = GetDisasm(here())
if "ret" in line:
break
if decoded:
function_name = get_string(GetRegValue("EAX"))
MakeComm(here(), function_name)
decoded = False
if get_operand_type(here(), 0) == MEM_REG:
offset = line[line.index("+")+1:line.index("]")-1]
try: # in case the struct was already identified, the cast would fail (e.g : int("R.CreateThread", 16))
print("offset {:x}".format(int(offset, 16)))
except:
pass
try:
idaapi.set_member_name(r, int(offset, 16), function_name)
except:
print("Failed to set struct")
function_name = ""
if "decodeString" in line:
decoded = True
StepOver();
GetDebuggerEvent(WFNE_SUSP, -1);
After execution and some retype of variables, we get a pretty decent result :
Once everything is resolved, the binary tries to detect any debug, VM or sandbox attempt in multiple functions:
Those functions include anti dbg trick based on loaded dlls (dbghelper.dll
), tries to check if it is in a sandbox based on different names and SIDs, but also checking if sandboxie’s dll is present. There are 3 anti-vm techniques used using cpuid
, sidt
, sgdt
. The program also checks if it is being executed from wine
.
Once then, it checks is the argument wusaupdate is passed on the command line (and it’s not) :
In the first function we can easily see that it tries to permanently shut down windows defender and stop any windows update :
Giving us easy IOCs - see annex.
On the next function, it will block many ports using window’s firewall :
This one could hardly be taken as an IOC because a legitimate program could use this command.
As I mentioned earlier, those commands wont be executed anyway as no argument is passed to the program.
To analyze the rest of the program, I set a breakpoint to one of the last lines of the main function that looked like a jump to some OEP :
Executing it gives a write error exception in a previous function call. I launch x86dbg to investigate and get stop on the write error exception:
From this screenshot we can notice that the program tries to write the letter M
somewhere in memory, right in the .rsrc
section (a resource inside the PE file obviously). This looks quite suspicious, could this be the start of MZ
magic bytes?
Spoiler : it is.
Modifying the rights on the memory pages used by the resource, and letting the function complete, we can see a nice PE file in memory dump :
I will dump this file, resolve its import and analyze it in Random Malware Analysis & Unpacking - Stage 3/3.
Annex - IOCs
/C Reg add \"HKEY_LOCAL_MACHINE\\SOFTWARE\\Policies\\Microsoft\\Windows Defender\" /v DisableAntiSpyware /t REG_DWORD /d 1 /f
/C sc config wuauserv start= disabled
/C sc stop wuauserv