Post

0x01_Lab01-01-part01

Overview

FilenameSizeMD5
Lab01-01.exe16 KBbb7425b82141a1c0f7d60e5106676bb1
Lab01-01.dll160 KB290934c61de9176ad682ffdd65f0a669

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.dll and Lab01-01.dll into its own address space
  • Rebuilding theIMAGE_EXPORT_DIRECTORY of Lab01-01.dll
  • Rebuilding the ExportAddressTable, ExportOrdinalTable, ExportNamePointerTable, and ExportNameTable of Lab01-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:

  1. 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 ; 
    
  2. 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
    
  3. 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:

  • RVA is the value to convert
  • ntHeaders points to the PE\x00\x00 signature
  • imageBase is the base address of the mapped view

The 5 calls allow retrieving the addresses of:

  • Export Directory of kernel32.dll
  • Export Directory of Lab01-01.dll
  • AddressOfFunctions of kernel32.dll
  • AddressesofNames of kernel32.dll
  • AddressesOfNameOrdinals of 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 SectionAlignment field (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 address

In our case, however, both kernel32.dll and Lab01-01.dll are mapped with calls to MapViewOfFile, 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 the FileAlignment field (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 renamed LocateHeaderOfSectionContainingRVA):

1
2
3
4
0x00401049    push    eax ; ->NtHeaders of kernel32
0x0040104A    push    esi ; RVA to convert
0x0040104B    call    LocateHeaderOfSectionContainingRVA
0x00401050    mov     ecx, eax

If we step into the function LocateHeaderOfSectionContainingRVA (code not shown), we see it parses an array of _IMAGE_SECTION_HEADER structures until the following condition matches:

1
_IMAGE_SECTION_HEADER.VirtualAddress <= RVA < (_IMAGE_SECTION_HEADER.Misc.VirtualAddress + _IMAGE_SECTION_HEADER.VirtualSize

A pointer to the _IMAGE_SECTION_HEADER matching 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 esi

That is:

1
(_IMAGE_SECTION_HEADER.PointerToRawData - _IMAGE_SECTION_HEADER.VirtualAddress) + RVA

In my opinion, this is more understandable if written this way:

1
(RVA - _IMAGE_SECTION_HEADER.VirtualAddress) + _IMAGE_SECTION_HEADER.PointerToRawData

So, 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    retn

In 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:

AND01
000
101

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 ComputeMagicDelta has the same prototype as the function RVA2MappedAddress seen previously. Digging into it, we see that:

Step 1: A pointer to the _IMAGE_SECTION_HEADER of the section containing the IMAGE_EXPORT_DIRECTORY is 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, eax

Step 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    retn

Note 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

This post is licensed under CC BY 4.0 by the author.