Team82 Logo Claroty
Return to Team82 Research

Delving Into Windows CE, Part 2: Analyzing Windows CE Debugging Constructs

/

This is part 2 of our four-part series on our analysis of the Windows CE attack surface, a legacy OS still found in many operational technology environments. In part 1 of this series, we wrote about simple Windows CE application development, which helped us understand the OS and further our research. 

While developing the Windows CE application described in part 1, we required the ability to debug it. Because we utilized Visual Studio 2005 to do so, we became curious about its debugging construct and decided to dig deeper into this aspect of application development. During our research, we discovered two interesting proprietary control and debugging protocols that we analyzed. In this blog, we want to share our research process that was later used to build custom research utilities.

Using the Visual Studio Debugging Utility

Visual Studio comes bundled with a built-in debugging utility allowing you to debug remote device applications over ethernet. This utility expects to connect to debugging agent services executed on the remote device. These services are implemented in binaries provided in the installation of Visual Studio for a wide variety of architectures.

Debugger service agents folders.

In our case, we need the armv4i architecture binaries.

Remote Debugger service binaries.

Here is a rundown of the remote debugger service binaries:

  • clisenshutdown.exe: Shut down commanding agent service

  • CMAccept.exe: Make commanding agent service accept incoming client connections

  • CommanClient2.exe: Execute commanding agent service

  • DeviceDMA.dll: Device memory manipulation-related routines library

  • eDbgTL.dll: Debugging library

  • TcpConnectionA.dll: Network-related routines library

  • Edm.exe: Agent implementing debugging routines

To start debugging a native application on a remote Windows CE device, we need to upload these binaries and execute them before trying to launch a debugging connection over ethernet.

To do so, we execute the following binaries in order:

  • Clientshutdown.exe:Close previous hanging connections

  • CommanClient2.exe: Start command controls service

  • CMAccept.exe: Enable accepting of new debugger connections

The next step is to configure our Visual Studio IDE to connect with the correct device over a network connection.

Configure debugger options.

And now we can start a remote debugger connection to the device.

Connecting to the target device.

Now we are able to launch our application in debug mode, or even attach it to an already running application on the device.

Processes running the target device.

How Does the VS Debugging Construct Work?

Even though we were able to debug an application using Visual Studio, we were still curious about how this construct works and how debugging actually works on Windows CE platforms.

We started off by reviewing the configuration of the remote debugger agent.

Equipped with this knowledge we used Wireshark to take a pcap of the network traffic between the debugger agent and Visual Studio.

Following network capture

Analyzing the network traffic and the state of our debugged system we managed to understand some parts of the protocol implementation. Some of these include:

  • Verify if file exists

  • Send file functionality

  • Start process

  • Terminate process

As we unraveled the protocol implementation, we noticed that our debugger interactions were missing from the filtered traffic. Using Wireshark's Statistics → Endpoints utility to verify if there are any other interesting endpoints, we noticed TCP port 6510.

TCP ports related to debugging

Adding port tcp.port==6510 as a filter, we followed the network traffic caused by our debugger interactions.

Following remote debugging capture.

Analyzing this protocol was simple because the request packets contain descriptive functionality strings such as BeginDebugSession, NativeDebugLaunch, GetEvents and more. 

For example, analyzing the packet in charge of reading a memory block from the process, you can see it requires three parameters:

  • PID: Process ID

  • Address: Memory read address

  • Memory-Size: The size of the memory block to read

ProcessMemoryRead packet format

Windows Native Debugging API

As we unraveled the protocol used to debug a native process on the device, it became clear that this is actually an RPC network interface wrapping native Windows debugging API functions. We decided to learn more about these API functions to have a better understanding of the RPC interface we are provided with. To do so we used a trusty source of information— MSDN—and decided to put these API function specifications in this document.


DebugActiveProcess

This function allows a debugger to attach to an active process and then debug it.

BOOL DebugActiveProcess( DWORD dwProcessId );

Parameters

dwProcessId: Specifies the identifier for the process to be debugged. The debugger gets debugging access to the process as if it created the process with the DEBUG_ONLY_THIS_PROCESS flag. See the Remarks section for more details.

Return Value

Nonzero indicates success. Zero indicates failure. To get extended error information, call GetLastError.

WaitForDebugEvent

This function waits for a debugging event to occur in a process being debugged.

BOOL WaitForDebugEvent( LPDEBUG_EVENT lpDebugEvent, DWORD dwMilliseconds );

Parameters

  • lpDebugEvent: Pointer to a DEBUG_EVENT structure that is filled with information about the debugging event.

  • dwMilliseconds: Specifies the number of milliseconds to wait for a debugging event. If this parameter is zero, the function tests for a debugging event and returns immediately. If the parameter is INFINITE, the function does not return until a debugging event has occurred.

Return Values

Nonzero indicates success. Zero indicates failure. To get extended error information, call GetLastError.

SetThreadContext

This function sets the context in the specified thread.

BOOL SetThreadContext( HANDLE hThread, CONST CONTEXT * lpContext );

Parameters

  • hThread: Handle to the thread whose context is to be set.

  • lpContext: Pointer to the CONTEXT structure that contains the context to be set in the specified thread. The value of the ContextFlags member of this structure specifies which portions of a threads context to set. Some values in the CONTEXT structure that cannot be specified are silently set to the correct value. This includes bits in the CPU status register that specify the privileged processor mode, global enabling bits in the debugging register, and other states that must be controlled by the operating system.

Return Values

Nonzero indicates that the context was set. Zero indicates failure. To get extended error information, call GetLastError.

GetThreadContext

This function retrieves the context of the specified thread.

BOOL GetThreadContext( HANDLE hThread, LPCONTEXT lpContext );

Parameters

  • hThread: Handle to the thread whose context is to be retrieved.

  • lpContext: Pointer to the CONTEXT structure that receives the appropriate context of the specified thread. The value of the ContextFlags member of this structure specifies which portions of a threads context are retrieved. The CONTEXT structure is highly computer specific. Currently, there are CONTEXT structures defined for Intel, MIPS, Alpha, ARM, SHx, and PowerPC processors. Refer to the header file WINNT.H for definitions of these structures.

Return Values

Nonzero indicates success. Zero indicates failure. To get extended error information, call GetLastError.

WriteProcessMemory

This function writes memory in a specified process. The entire area to be written to must be accessible, or the operation fails.

BOOL WriteProcessMemory( HANDLE hProcess, LPVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesWritten );

Parameters

  • hProcess: Handle returned from the OpenProcess function that provided full access to the process.

  • lpBaseAddress: Pointer to the base address in the specified process to be written to. Before any data transfer occurs, the system verifies that all data in the base address and memory of the specified size is accessible for write access. If this is the case, the function proceeds; otherwise, the function fails.

  • lpBuffer: Pointer to the buffer that supplies data to be written into the address space of the specified process.

  • nSize: Specifies the requested number of bytes to write into the specified process.

  • lpNumberOfBytesWritten: Pointer to the actual number of bytes transferred into the specified process. This parameter is optional. If lpNumberOfBytesWritten is NULL, the parameter is ignored.

Return Values

Nonzero indicates success. Zero indicates failure. To get extended error information, call GetLastError. The function fails if the requested write operation crosses into an area of the process that is inaccessible.

ReadProcessMemory

This function reads memory in a specified process. The entire area to be read must be accessible, or the operation fails.

BOOL ReadProcessMemory( HANDLE hProcess, LPCVOID lpBaseAddress, LPVOID lpBuffer, DWORD nSize, LPDWORD lpNumberOfBytesRead );

Parameters

  • hProcess: Handle to the process whose memory is being read. In Windows CE, any call to OpenProcess will return a process handle with the proper access rights.

  • lpBaseAddress: Pointer to the base address in the specified process to be read. Before any data transfer occurs, the system verifies that all data in the base address and memory of the specified size is accessible for read access. If this is the case, the function proceeds; otherwise, the function fails.

  • lpBuffer: Pointer to a buffer that receives the contents from the address space of the specified process.

  • nSize: Specifies the requested number of bytes to read from the specified process.

  • lpNumberOfBytesRead: Pointer to the actual number of bytes transferred into the specified buffer. If lpNumberOfBytesRead is NULL, the parameter is ignored.

Return Values

Nonzero indicates success. Zero indicates failure. To get extended error information, call GetLastError.

What’s to Come: Building A Debugger Client

As we went further with researching these protocols, we started to experiment with building client scripts to interact with the processes we debugged. This became very useful when debugging researched applications. Being able to debug a Windows CE application from our own Linux host machine and having the ability to customize the experience of researching this type of device was a great benefit. Read more about this in part 3.

Stay in the know Get the Team82 Newsletter
Recent Vulnerability Disclosures
Claroty
LinkedIn Twitter YouTube Facebook