0x01_Lab01-01-part01
Overview
| Filename | Size | MD5 |
|---|---|---|
| Lab01-01.exe | 16 KB | bb7425b82141a1c0f7d60e5106676bb1 |
| Lab01-01.dll | 160 KB | 290934c61de9176ad682ffdd65f0a669 |
TL;DR: The malware comes in two parts: a small executable (analyzed here) and a DLL without export table (analyzed in part2). The malicious EXE rebuilds the export table of the DLL and modifies part of the filesystem so the DLL is loaded when executables from the folder C:\Windows\System32\ are executed. The malicious DLL creates a mutex named SADFHUHF, opens a socket to 127.26.152.13:80, sends a hello beacon and processes the commands it eventually receives from a remote attacker.
Tools: IDA Free 7.0, x32dbg
IDB: Lab01-01_exe.i64
Anti-fatfingers
A check of the commandline is implemented to avoid the accidental execution of the malware. First, argc and argv are passed to the function _main():
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
0x004018BF lea eax, [ebp+_StartupInfo]
0x004018C2 push eax
0x004018C3 push _DoWildCard
0x004018C9 lea eax, [ebp+env]
0x004018CC push eax
0x004018CD lea eax, [ebp+argv]
0x004018D0 push eax
0x004018D1 lea eax, [ebp+argc]
0x004018D4 push eax
0x004018D5 call ds:__getmainargs
[...]
0x004018F5 push [ebp+envp]
0x004018F8 push [ebp+argv]
0x004018FB push [ebp+argc]
0x004018FE call _main
Within the _main() function, the code checks if the commandline has exactly 2 arguments:
1
2
3
4
5
6
7
8
0x00401440 mov eax, [esp+argc]
0x00401444 sub esp, 44h
0x00401447 cmp eax, 2 ; check
0x0040144A push ebx
0x0040144B push ebp
0x0040144C push esi
0x0040144D push edi
0x0040144E jnz quit ; exit if argc != 2
And it checks if the second argument matches the string WARNING_THIS_WILL_DESTROY_YOUR_MACHINE:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
0x00401454 mov eax, [esp+54h+argv]
0x00401458 mov esi, offset aWarni ; WARNING_THIS_WILL_DESTROY_YOUR_MACHINE
0x0040145D mov eax, [eax+4] ; ptrArg n°2
0x00401460
0x00401460 next_char:
0x00401460 mov dl, [eax] ; Arg n°2 (char)
0x00401462 mov bl, [esi] ; string looked for
0x00401464 mov cl, dl
0x00401466 cmp dl, bl ; check char
0x00401468 jnz short dont_match ; quit
0x0040146A test cl, cl
0x0040146C jz short got_end_of_string
0x0040146E mov dl, [eax+1]
0x00401471 mov bl, [esi+1]
0x00401474 mov cl, dl
0x00401476 cmp dl, bl
0x00401478 jnz short dont_match ; quit
0x0040147A add eax, 2
0x0040147D add esi, 2
0x00401480 test cl, cl
0x00401482 jnz short next_char
0x00401484 got_end_of_string:
0x00401484 xor eax, eax ; good boy
0x00401486 jmp short loc_40148D
If both strings match, the code reaches address 0x401484. There, eax is set to zero and the code jumps to 0x40148D. This check can be bypassed in many ways. I’ve chosen to patch the conditional jnz of the first check with this:
1
2
0x0040144E xor eax, eax
0x00401450 jmp short loc_40148D
Rebuilding the malicious DLL
The malicious executable uses the legitimate kernel32.dll found on the system to rebuild the malicious DLL. The 3 main steps are:
- Mapping both
kernel32.dllandLab01-01.dllinto its own address space - Rebuilding the
IMAGE_EXPORT_DIRECTORYofLab01-01.dll - Rebuilding the
ExportAddressTable,ExportOrdinalTable,ExportNamePointerTable, andExportNameTableofLab01-01.dll
Mapping kernel32.dll and Lab01-01.dll
Both DLLs are mapped using the following 3 APIs: CreateFileA, CreateFileMappingA, and MapViewOfFile. The legitimate kernel32.dll is mapped with read permission, while Lab01-01.dll is mapped with read and write permissions. Snippets below detail the mapping of kernel32.dll:
- First, get a file handle with read permission:
1 2 3 4 5 6 7 8 9 10
0x0040149B push eax ; hTemplateFile 0x0040149C push eax ; dwFlagsAndAttributes 0x0040149D push OPEN_EXISTING ; dwCreationDisposition 0x0040149F push eax ; lpSecurityAttributes 0x004014A0 push FILE_SHARE_READ ; dwShareMode 0x004014A2 push GENERIC_READ ; dwDesiredAccess 0x004014A7 push offset FileName ; path to kernel32.dll 0x004014AC call edi ; CreateFileA [...] 0x004014BF mov [esp+6Ch+hFile_Kernel32_DLL], eax ;
- Next, create a file mapping object:
1 2 3 4 5 6 7 8
0x004014B4 push 0 ; lpName 0x004014B6 push 0 ; dwMaximumSizeLow 0x004014B8 push 0 ; dwMaximumSizeHigh 0x004014BA push PAGE_READONLY ; flProtect 0x004014BC push 0 ; lpFileMappingAttributes 0x004014BE push eax ; hFile [...] 0x004014C3 call ebx ; CreateFileMappingA
- Finally, map a view of the DLL:
1 2 3 4 5 6 7 8 9 10
0x004014CB push 0 ; dwNumberOfBytesToMap 0x004014CD push 0 ; dwFileOffsetLow 0x004014CF push 0 ; dwFileOffsetHigh 0x004014D1 push FILE_MAP_READ ; dwDesiredAccess 0x004014D3 push eax ; hFileMappingObject 0x004014D4 call ebp ; MapViewOfFile [...] 0x004014E0 mov esi, eax [...] 0x004014EC mov [esp+70h+address_mapped_k32], esi
As stated above, the same 3 APIs are used to map the malicious DLL with read and write permissions (code not shown for brevity). An important thing to note is we have a 1:1 correspondence between the mapped view and the file on disk, meaning both will be aligned according to the FileAlignment field of the PE. (If the DLLs were loaded using LoadLibraryA, their memory image would have been aligned according to the SectionAlignment field of the PE). That being said, let’s continue after the second call to MapViewofFile (seen at address 0x401525).
Rebuilding the IMAGE_EXPORT_DIRECTORY
Retrieving important fields
The function 0x401040 is called 5 times. Below illustrates the first call:
1
2
3
4
5
6
7
8
9
10
; esi = base address of mapped kernel32.dll
0x00401538 map_success:
0x00401538 mov edi, [esi+3Ch] ; e_lfanew
0x0040153B push esi ; baseMappedView
0x0040153C add edi, esi ; Kernel32->NtHeaders
0x0040153E push edi ; ptrNtHeaders
0x0040153F mov [esp+5Ch+pNtHeaders], edi
0x00401543 mov eax, [edi+78h] ; Export Directory RVA
0x00401546 push eax ; RVA
0x00401547 call RVA2MappedAddress ;
After some digging, its purpose appears: converting a given RVA to a mapped address. Here is the prototype I came with:
1
2
3
4
5
6
BYTE*
RVA2MappedAddress(
DWORD RVA,
_IMAGE_NT_HEADERS* ntHeaders,
BYTE* imageBase
);
Where:
RVAis the value to convertntHeaderspoints to thePE\x00\x00signatureimageBaseis the base address of the mapped view
The 5 calls allow retrieving the addresses of:
Export Directoryof kernel32.dllExport Directoryof Lab01-01.dllAddressOfFunctionsof kernel32.dllAddressesofNamesof kernel32.dllAddressesOfNameOrdinalsof kernel32.dll
In addition, both NumberOfFunctions and NumberOfNames of kernel32.dll are retrieved.
Rabbit hole: converting a RVA to a mapped address
When a module is loaded into memory (e.g. with a call to LoadLibraryA), its sections are aligned according to the
SectionAlignmentfield (default = page size, 0x1000 on my system). Hence, to get a virtual address from a RVA all we have to do is:
1 RVA + ImageBase = virtual addressIn our case, however, both
kernel32.dllandLab01-01.dllare mapped with calls toMapViewOfFile, meaning we have a 1:1 correspondence between a mapped image and its corresponding file on disk. This implies the mapped image is aligned according to theFileAlignmentfield (default is 0x200). Consequently, to compute the address indicated by a given RVA, we need to take into account these alignment differences. This is done in 3 steps:Step 1: Identify the section containing the value of the RVA: it is the purpose of the function
0x401000(here renamedLocateHeaderOfSectionContainingRVA):
1 2 3 4 0x00401049 push eax ; ->NtHeaders of kernel32 0x0040104A push esi ; RVA to convert 0x0040104B call LocateHeaderOfSectionContainingRVA 0x00401050 mov ecx, eaxIf we step into the function
LocateHeaderOfSectionContainingRVA(code not shown), we see it parses an array of_IMAGE_SECTION_HEADERstructures until the following condition matches:
1 _IMAGE_SECTION_HEADER.VirtualAddress <= RVA < (_IMAGE_SECTION_HEADER.Misc.VirtualAddress + _IMAGE_SECTION_HEADER.VirtualSizeA pointer to the
_IMAGE_SECTION_HEADERmatching the above condition is returned.Step 2: Then, the RVA is converted to a raw offset:
1 2 3 4 5 6 7 8 ; ecx = pointer to _IMAGE_SECTION_HEADER ; esi = RVA to convert 0x0040105B mov eax, [ecx+14h] ; section raw address 0x0040105E mov edx, [ecx+0Ch] ; section virtual address 0x00401061 mov ecx, [esp+4+baseMappedView] 0x00401065 sub eax, edx 0x00401067 add eax, esi ; (rawaddr-vaddr)+RVA=RVAToOffset 0x00401069 pop esi ; restore esiThat is:
1 (_IMAGE_SECTION_HEADER.PointerToRawData - _IMAGE_SECTION_HEADER.VirtualAddress) + RVAIn my opinion, this is more understandable if written this way:
1 (RVA - _IMAGE_SECTION_HEADER.VirtualAddress) + _IMAGE_SECTION_HEADER.PointerToRawDataSo, first we compute RVA - _IMAGE_SECTION_HEADER.VirtualAddress, which gives us an offset from the start of the section. Then, because _IMAGE_SECTION_HEADER.PointerToRawData is the offset of the section from the start of the file, we simply add these two values to get to correct offset.
Step 3: Finally, the base address of the mapped image is added to the offset:
1 2 3 4 ; eax = offset ; ecx = base of the mapped image 0x0040106A add eax, ecx ; mapped address 0x0040106C retnIn the end, we have the right mapped address.
Copy kernel32.dll export table to Lab01-01.dll
Right now, the export table of Lab01-01.dll is empty. So, Lab01-01.exe will copy the export table of kernel32.dll over the empty one. The copy is made with the following code:
1
2
3
4
5
6
0x004015B5 mov ecx, edi ; size of export directory
0x004015B7 mov esi, ebx ; kernel32 export directory
0x004015B9 mov edx, ecx ; export directory size
0x004015BB mov edi, ebp ; malicious export directory
0x004015BD shr ecx, 2 ; size/4 = nb dword
0x004015C0 rep movsd ; overwrite
Instruction rep movsd copies n=ecx dwords from the source address pointed by esi to the destination address pointed by edi. The value of ecx comes from IMAGE_DATA_DIRECTORY[0].Size (0xA9B1. Code not shown, but it’s the value at PE+0x7C). Because movsd is used, ecx has to be divided by 4 (1 dword = 4 bytes); this is the purpose instruction shr ecx, 2. Indeed: shr x, y = x/(2**y) (and shl x, y = x*(y**2)).
If the overall size to copy is not dword-aligned (i.e. between 1 and 3 bytes remain to be copied), the remaining bytes are copied with instruction rep movsb, which copy n=ecx bytes from esi to edi:
1
2
3
4
5
0x004015C2 mov ecx, edx : size of export directory
[...]
0x004015C7 and ecx, 3 ; how many last bytes
[...]
0x004015CE rep movsb ; copy
The magic happens thanks to the instruction and ecx, 3. Below is the AND truth table:
| AND | 0 | 1 |
|---|---|---|
| 0 | 0 | 0 |
| 1 | 0 | 1 |
Hence, no mater the value of ecx (number of bytes to copy), the and ecx, 3 sets all bits to 0 excepting the last two least significant ones:
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx00 & 3 = 0 byte (nothing to copy)
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx01 & 3 = 1 byte to copy
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx10 & 3 = 2 bytes to copy
- xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx11 & 3 = 3 bytes to copy
Updating fields of the IMAGE_EXPORT_DIRECTORY (1)
In the code below, both NumberOfFunctions and NumberOfNames are copied from kernel32.dll to Lab01-01.dll:
1
2
3
4
5
6
7
; ebx->Export directory of kernel32.dll
; ebp->Export directory of malicious dll
0x004015D0 mov ecx, [ebx+14h] ; NbOfFunctions (k32)
0x004015D3 mov [ebp+14h], ecx ; update NumberOfFunctions
0x004015D6 mov edx, [ebx+18h] ; NumberOfNames (k32)
[...]
0x004015DC mov [ebp+18h], edx ; update NumberOfNames
Then comes the baptism of the malicious DLL: the string “kerne123.dll” is written just after its export directory with several mov instructions (note the number “1” instead of the lowercase “l”) :
1
2
3
4
5
6
7
8
9
10
11
12
13
; ebp->Export directory of malicious dll
0x004015D9 lea ebx, [ebp+28h] ; end of export directory
[...]
0x004015E8 mov esi, dword ptr fakename ; "kerne132.dll"
0x004015EE mov edx, ebx
[...]
0x004015F7 mov [edx], esi
0x004015F9 mov esi, dword ptr fakename+4 ; "e132.dll"
0x004015FF mov [edx+4], esi
0x00401602 mov esi, dword ptr fakename+8 ; ".dll"
0x00401608 mov [edx+8], esi
0x0040160B mov esi, dword ptr fakename+0Ch ; ""
0x00401611 mov [edx+0Ch], esi
The field NameRVA has to be updated so the RVA will indicate the new name. Recall the rabbit hole where RVAs were converted to mapped addresses? Well, we have to go the other way now: the mapped address pointing to the string “kerne123.dll” (new name of the malicious DLL) is converted to an RVA. The function we are interested in is called at address 0x4015B0:
1
2
3
4
5
6
; esi->NtHeaders
0x004015AA mov eax, [esi+78h] ; Exp. directory RVA (malicious)
0x004015AD push edx ; baseMappedView
0x004015AE push esi ; malicious->NtHeaders
0x004015AF push eax ; RVA
0x004015B0 call ComputeMagicDelta
The magic delta computed will be used to convert any mapped address to an RVA. The way it has been implemented perplexed me, but it does the job. Below is the second and last rabbit hole of this writeup.
Rabbit hole: converting a mapped address to an RVA
Instinctively, I was expecting a similar approach to what we have seen in the first rabbit hole (see there for example). In this malware, however, author(s) have opted for a kind of “magic delta”. I guess Forrest Gump’s mom would have had something to say about that.
The function
ComputeMagicDeltahas the same prototype as the functionRVA2MappedAddressseen previously. Digging into it, we see that:Step 1: A pointer to the
_IMAGE_SECTION_HEADERof the section containing theIMAGE_EXPORT_DIRECTORYis retrieved:
1 2 3 4 5 6 0x00401070 mov eax, [esp+pNtHeaders] 0x00401074 mov ecx, [esp+RVA] ; exp. directory (malicious) 0x00401078 push eax 0x00401079 push ecx 0x0040107A call LocateHeaderOfSectionContainingRVA 0x0040107F mov ecx, eaxStep 2: The delta between the RVA of the section and the raw offset of the section is computed:
1 2 3 4 5 6 7 8 ; ecx->_IMAGE_SECTION_HEADER 0x00401089 loc_401089: 0x00401089 mov eax, [ecx+0Ch] ; section VirtualAddress 0x0040108C mov edx, [ecx+14h] ; section PointerToRawData 0x0040108F mov ecx, [esp+baseMappedView] 0x00401093 sub eax, edx ; vaddr - raddr 0x00401095 sub eax, ecx ; (vaddr - raddr) - base 0x00401097 retnNote the base address of the mapped view is also substracted. The resulting value will be used later to convert any mapped address (as long as it points within the section containing the export table) to an RVA.
Updating fields of the IMAGE_EXPORT_DIRECTORY (2)
Now the magic delta to convert mapped addresses to RVAs has been computed, the fields NameRVA, AddressOfFunctions, AddressOfNames and AddressOfNameOrdinals can be filled.
The code below shows how the field NameRVA is updated:
1
2
3
4
5
6
7
8
; ebp->Export directory of malicious dll
; eax = magic delta
0x004015D9 lea ebx, [ebp+28h] ; ebx->"kerne123.dll"
[...]
0x004015E2 lea edx, [ebx+eax] ; convert to RVA
0x004015E5 mov [ebp+0Ch], edx ; update field
[...]
0x004015F0 add ebx, 10h ; ->end of string + dw align.
The add ebx, 10h instruction allows having ebx pointing after the string “kerne132.dll” (plus some padding to be dword-aligned). Half-spoil, let’s call the address pointed by ebx array1.
Two other addresses are computed, let’s call them array2 and array3:
1
2
3
4
5
6
7
8
; ebp->malicious export directory
; ebx->array1
0x00401614 mov edx, [ebp+14h] ; NumberOfFuntion
[...]
0x00401617 lea esi, [ebx+edx*4] ; ->array2
0x0040161A lea edi, [ebx+edx*8] ; ->array3
0x0040161D mov [esp+54h+array2], esi ;
0x00401621 mov [esp+54h+array3], edi ;
Next, the magic delta is applied to array1, array2 and array3; their respective purpose appears in the following snippet:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; ebp->malicious export directory
; eax = magic delta
; ebx->array1
; ecx = NumberOfFunctions
; edi->array3
; esi->array2
0x004015DF shl ecx, 4 ; NbOfFunction*16
[...]
0x00401625 lea edx, [ebx+eax]
0x00401628 add ebx, ecx ; ->array4
0x0040162A mov [ebp+1Ch], edx ; AddrofFunctions RVA
0x0040162D lea edx, [esi+eax]
0x00401630 add eax, edi
0x00401632 mov [ebp+24h], edx ; AddrofNameOrdinals RVA
0x00401635 mov [ebp+20h], eax ; AddrofNames RVA
And suddently, the light:
- array1 = malicious
ExportAddressTable - array2 = malicious
ExportOrdinalTable - array3 = malicious
ExportNamePointerTable
For a refreasher, I made cheatsheet about the export table. The code above also shows a fourth array (array4) pointing to array1 + (NbOfFunctions*16) is computed. This is the future ExportNameTable, an array of strings that will contain a copy of all API names exported by kernel32.dll.
At this point, the IMAGE_EXPORT_DIRECTORY of the malicious DLL is rebuilt. Now is time to rebuild the ExportAddressTable, ExportOrdinalTable, ExportNamePointerTable, and ExportNameTable.
Rebuild the EAT, ENPT, EOT, and ENT
A new call to RVA2MappedAddress occurs at address 0x004016c1. This call retrieves a pointer to the first API name exported by kernel32.dll; then, the length of the string is computed thanks to the instruction repne scasb:
1
2
3
4
5
6
7
8
9
0x004016C1 call RVA2MappedAddress
0x004016C6 mov edx, eax ; ->API name
0x004016C8 or ecx, 0FFFFFFFFh ; repne will decrease ecx
0x004016CB mov edi, edx ; ->string (API name)
0x004016CD xor eax, eax ; value looked for
[...]
0x004016D4 repne scasb ; scan while \x00 not found
0x004016D6 not ecx ; length (incl. \x00)
0x004016D8 mov eax, ecx
Once the length of the API name is known, the string is copied into the ExportNameTable of the malicious DLL (called array4 in the previous section):
1
2
3
4
5
6
7
8
9
10
11
; esi->API name in kernel32.dll
; ecx = string length
; ebx->ExportNameTable (API name) in malicious DLL
0x004016D8 mov eax, ecx
0x004016DA mov edi, ebx
0x004016DC shr ecx, 2 ; length/4 because movsd
0x004016DF rep movsd
0x004016E1 mov ecx, eax
[...]
0x004016E7 and ecx, 3 ; copy remaining
0x004016EA rep movsb
Now the first API name (i.e. first entry of the ExportNameTable) has been copied, its corresponding RVA has to be set in the ExportNamePointerTable:
1
2
3
4
5
6
7
; ebp->ExportAddressTable
; eax = size of ExportAddressTable
; ebx->ExportNameTable (copied API name)
; esi = magic delta
0x004016FC lea ecx, [ebx+esi] ; RVA of API name
[...]
0x00401701 mov [eax+ebp], ecx ; ->EAT+sizeof(EAT) = ->ENPT
From the snipped above we can also say that the ExportNamePointerTable is found right after the ExportAddressTable.
Once the first entry of the ExportNamePointerTable is good, the first entry of the ExportOrdinalTable is set:
1
2
3
4
5
0x004016E3 mov eax, [esp+54h+EOT]
[...]
0x004016EC mov cx, word ptr [esp+54h+counter]
[...]
0x004016F5 mov [eax], cx ; new NameOrdinal
Last but not least, the ExportAddressTable is updated. What happens here is the setup of export forwarding: where we may expect to find the RVA of the “entrypoint” of the first API, we will instead have the RVA of a string matching the pattern “kernel32.apiname”:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; ebx->ExportNameTable (copied API name)
; ecx=string length (without '\x00')
; esi = magic delta
0x00401710 lea ebx, [ebx+ecx+1] ; ->end of apiname+1 (\x00)
0x00401714 mov eax, ebx ; eax->start of new string
0x00401716 lea ecx, [ebx+esi] ; ecx = RVA of new string
0x00401719 add ebx, 9 ; ebx+=len("kernel32.")
0x0040171C mov [ebp+0], ecx ; update EAT
0x0040171F mov ecx, dword ptr aKernel32 ; write "Kern"
0x00401725 mov [eax], ecx
0x00401727 mov ecx, dword ptr aKernel32+4 ; write "el32"
[...]
0x0040172F mov [eax+4], ecx
0x00401732 mov cl, byte ptr aKernel32+8 ; write "."
0x00401738 mov [eax+8], cl ; 'Kernel32.'
At this point, the first entry of the ExportAddressTable is the RVA of the string “Kernel32.”. The last step is to write the name of the API right after this string:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; edi->API name (in kernel32.dll)
; esi->API name (in kernel32.dll)
; ebx->empty space (just after the string "kernel32.")
0x0040173B or ecx, 0FFFFFFFFh
0x0040173E xor eax, eax
0x00401740 repne scasb
0x00401742 not ecx
0x00401744 mov eax, ecx ; API name length
0x00401746 mov edi, ebx ; ->after "kernel32."
0x00401748 shr ecx, 2 ; copy dwords
0x0040174B rep movsd
0x0040174D mov ecx, eax ; API name length
0x0040174F xor eax, eax
0x00401751 and ecx, 3 ; copy remaining bytes
0x00401754 rep movsb
All the rep*-based shenanigans have been explained in previous sections. After that, the different counters and pointers are updated, and the next API is processed (code not shown).
So, we finally have the first entry of the ExportAddressTable being the RVA of a string following the pattern “Kernel32.apiname”. This, plus the fact that the RVA relates to an address inside the export table (and not within the code section) indicates export forwarding. That is, the export table of the malicious DLL has been rebuilt to forward all calls made to it to the legitimate kernel32.dll.
But, why a given binary would call APIs exported by the malicious DLL? This is what we’ll see in the next section.
Alteration of the filesystem
Dropping the malicious DLL on disk
Now the malicious DLL is rebuilt, the malware writes it to the path C:\windows\system32\kerne123.dll (recall the number “1” instead of the letter “l” see earlier?):
1
2
3
4
0x004017E8 push 0 ; bFailIfExists
0x004017EA push offset NewFileName ; "C:\\windows\\system32\\kerne132.dll"
0x004017EF push offset ExistingFileName ; "Lab01-01.dll"
0x004017F4 call ds:CopyFileA
Searching for valid targets
A recursive function is called, its parameters are the string "C:\*" and the integer 0:
1
2
3
4
0x004017FC push 0 ; recursion_depth
[...]
0x00401806 push offset aC ; "C:\\*"
0x0040180B call RecurseFilesystem ; 0x4011E0
The function starts by comparing the integer parameter with the constant 7 and exits if greater:
1
2
3
4
5
0x004011E0 mov eax, [esp+recursion_depth]
0x004011E4 sub esp, 144h
0x004011EA cmp eax, 7
[...]
0x004011F1 jg exit
This constant is a level of recursion. If the level is less or equal to 7, the code flow reaches a call to the API FindFirstFileA:
1
2
3
4
5
0x004011F7 mov ebp, [esp+154h+lpFileName]
0x004011FE lea eax, [esp+154h+FindFileData]
0x00401202 push eax ; lpFindFileData
0x00401203 push ebp ; lpFileName ; "C:\\*"
0x00401204 call ds:FindFirstFileA
After this call, the return code is checked:
1
2
0x00401219 test byte ptr [esp+154h+FindFileData.dwFileAttributes], FILE_ATTRIBUTE_DIRECTORY
0x0040121E jz not_a_dir
If the call returns the value FILE_ATTRIBUTE_DIRECTORY (and the directory is neither "." nor ".."), the name of the found directory is concatenated with the parameter lpFileName (thus building a new path), the depth parameter is incremented, and the function recurses (code not shown).
If the call returns a file, the last 4 chars of its filename are compared with the hardcoded string “.exe”. If the comparison doesn’t match, the search continues with a call to FindNextFileA:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
0x0040136C lea ebx, [esp+ecx+154h+FindFileData.dwReserved1]
[...]
0x004013A push offset aExe ; ".exe"
[...]
0x004013AC push ebx ; 4 last chars of the filename
[...]
0x004013F6 call ds:_stricmp
[...]
0x004013FF test eax, eax
0x00401401 jnz short not_an_exe
0x00401403 push ebp ; lpFileName
0x00401404 call HijackImportTable ; hijack import table
0x00401409 add esp, 4
0x0040140C not_an_exe:
0x0040140C mov ebp, [esp+154h+lpFileName]
0x00401413 loc_401413:
0x00401413 mov esi, [esp+154h+hFindFile]
0x00401417 lea eax, [esp+154h+FindFileData]
0x0040141B push eax ; lpFindFileData
0x0040141C push esi ; hFindFile
0x0040141D call ds:FindNextFileA
However, if the file extension matches the string “.exe”, we reach address 0x00401404 and the function I’ve renamed HijackImportTable is called.
Hijacking import tables
A refresher on import table is available here.
When a valid target is found, the function HijackImportTable is called. It starts by mapping a view of the target file into memory with read and write permissions. This is similar to what we have seen earlier when the malicious DLL was mapped. Then, the PE signature of the target is checked:
1
2
3
4
5
; esi->mapped file
004010FA mov ebp, [esi+3Ch]
[...]
00401112 cmp dword ptr [ebp+0], 'EP'
00401119 jnz exit
If the signature is valid, the malware get the RVA of the import table of the target executable and converts it to a valid mapped address:
1
2
3
4
5
6
7
8
;ebp->NtHeaders.signature
0x0040111F mov ecx, [ebp+80h]
0x00401125 push esi ; baseMappedView
0x00401126 push ebp ; pNtHeaders
0x00401127 push ecx ; RVA
0x00401128 call RVA2MappedAddress
0x0040112D add esp, 0Ch
0x00401130 mov edi, eax ; ->IMAGE_IMPORT_DESCRIPTOR
This allows to retrieve a pointer to the first IMAGE_IMPORT_DESCRIPTOR structure of the target executable. Then, a pointer to the name of the DLL is retrieved from the value of the field IMAGE_IMPORT_DESCRIPTOR.NameRVA:
1
2
3
4
5
6
7
8
9
10
; edi->IMAGE_IMPORT_DESCRIPTOR
0x0040113F add edi, 0Ch ; NameRVA
[...]
0x00401152 mov edx, [edi]
0x00401154 push esi ; baseMappedView
0x00401155 push ebp ; pNtHeaders
0x00401156 push edx ; RVA
0x00401157 call RVA2MappedAddress
0x0040115C add esp, 0Ch
0x0040115F mov ebx, eax
The name of the DLL is compared with the string “kernel32.dll”:
1
2
3
4
5
6
7
; ebx->dll name
0x0040116E push offset Str2 ; "kernel32.dll"
0x00401173 push ebx ; dll name
0x00401174 call ds:_stricmp ; case insensitive comparison
0x0040117A add esp, 8
0x0040117D test eax, eax
0x0040117F jnz short not_k32_thunk
If the strings doesn’t match, the next IMAGE_IMPORT_DESCRIPTOR is checked. However, if the strings match, “kernel32.dll” is replaced by “kerne132.dll” (number “1” instead of letter “l”):
1
2
3
4
5
6
7
8
9
10
11
12
13
; ebx->dll name
0x00401181 mov edi, ebx
0x00401183 or ecx, 0FFFFFFFFh
0x00401186 repne scasb
0x00401188 not ecx
0x0040118A mov eax, ecx ; length
0x0040118C mov esi, offset fakename ; "kerne132.dll"
0x00401191 mov edi, ebx
0x00401193 shr ecx, 2 ; length/4
0x00401196 rep movsd ; copy dwords
0x00401198 mov ecx, eax
0x0040119A and ecx, 3
0x0040119D rep movsb ; copy remaining bytes
And that’s it, now the field IMAGE_IMPORT_DESCRIPTOR.NameRVA will be the RVA of the malicious DLL name, the DLL that has been copied in the C:\Windows\System32 folder. The next time one of the compromised executable in this folder will be executed, the malicious DLL will be loaded. And calls to APIs exported by the malicious DLL will be forwarded to the legitimate kernel32.dll.
The malicious DLL is analyzed in Part 2.
EOF