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.
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.
In our case, we need the armv4i
architecture 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.
And now we can start a remote debugger connection to the device.
Now we are able to launch our application in debug mode, or even attach it to an already running application on the device.
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.
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.
Adding port tcp.port==6510
as a filter, we followed the network traffic caused by our debugger interactions.
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
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.
This function allows a debugger to attach to an active process and then debug it.
BOOL DebugActiveProcess( DWORD dwProcessId );
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.
Nonzero indicates success. Zero indicates failure. To get extended error information, call GetLastError
.
This function waits for a debugging event to occur in a process being debugged.
BOOL WaitForDebugEvent( LPDEBUG_EVENT lpDebugEvent, DWORD dwMilliseconds );
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.
Nonzero indicates success. Zero indicates failure. To get extended error information, call GetLastError
.
This function sets the context in the specified thread.
BOOL SetThreadContext( HANDLE hThread, CONST CONTEXT * lpContext );
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.
Nonzero indicates that the context was set. Zero indicates failure. To get extended error information, call GetLastError
.
This function retrieves the context of the specified thread.
BOOL GetThreadContext( HANDLE hThread, LPCONTEXT lpContext );
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.
Nonzero indicates success. Zero indicates failure. To get extended error information, call GetLastError.
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 );
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.
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.
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 );
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.
Nonzero indicates success. Zero indicates failure. To get extended error information, call GetLastError
.
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.
CWE-547 USE OF HARD-CODED, SECURITY-RELEVANT CONSTANTS:
Optigo Networks Visual BACnet Capture Tool and Optigo Visual Networks Capture Tool version 3.1.2rc11 are vulnerable to an attacker impersonating the web application service and mislead victim clients.
Optigo Networks recommends users to upgrade to the following:
CVSS v3: 7.5
CWE-288 AUTHENTICATION BYPASS USING AN ALTERNATE PATH OR CHANNEL:
Optigo Networks Visual BACnet Capture Tool and Optigo Visual Networks Capture Tool version 3.1.2rc11 contain an exposed web management service that could allow an attacker to bypass authentication measures and gain controls over utilities within the products.
Optigo Networks recommends users to upgrade to the following:
CVSS v3: 9.8
CWE-547 USE OF HARD-CODED, SECURITY-RELEVANT CONSTANTS:
Optigo Networks Visual BACnet Capture Tool and Optigo Visual Networks Capture Tool version 3.1.2rc11 contain a hard coded secret key. This could allow an attacker to generate valid JWT (JSON Web Token) sessions.
Optigo Networks recommends users to upgrade to the following:
CVSS v3: 7.5
CWE-912 HIDDEN FUNCTIONALITY:
The "update" binary in the firmware of the affected product sends attempts to mount to a hard-coded, routable IP address, bypassing existing device network settings to do so. The function triggers if the 'C' button is pressed at a specific time during the boot process. If an attacker is able to control or impersonate this IP address, they could upload and overwrite files on the device.
Per FDA recommendation, CISA recommends users remove any Contec CMS8000 devices from their networks.
If asset owners cannot remove the devices from their networks, users should block 202.114.4.0/24 from their networks, or block 202.114.4.119 and 202.114.4.120.
Please note that this device may be re-labeled and sold by resellers.
Read more here: Do the CONTEC CMS8000 Patient Monitors Contain a Chinese Backdoor? The Reality is More Complicated….
CVSS v3: 7.5
CWE-295 IMPROPER CERTIFICATE VALIDATION:
The affected product is vulnerable due to failure of the update mechanism to verify the update server's certificate which could allow an attacker to alter network traffic and carry out a machine-in-the-middle attack (MITM). An attacker could modify the server's response and deliver a malicious update to the user.
Medixant recommends users download the v2025.1 or later version of their software.
CVSS v3: 5.7