Reverse-engineering a Wifi microscope


My newest "toy" is a microscope that I mainly bought to inspect PCBs and SMD solder joints. It was sold under the Rotek brand; however, I'm sure that the same thing is available under other names, as well. When connected to a PC via USB cable, this microscope works as a webcam would do. Therefore, you can use any application that can record off a webcam with it.

But the microscope is advertised with another feature: Wifi connection to an Android or iOS app. I was interested in seeing how that worked, mainly because the cable connection was sometimes unstable. (This turned out to be an unreliable connector on the cable that came with it. With a better quality USB cable, everything is fine.) I googled, expecting that someone had already reverse-engineered the protocol, but found nothing. This intrigued me even more. Thus, I set out to do it myself.

Initial inspection

The microscope creates an open Wifi access point named Max-See_xxxx. When you connect to it, your computer gets the IP address 192.168.29.101, and you can see that the microscope has the IP address 192.168.29.1. I opened a web browser and tried http://192.168.29.1. That opened a website served from the microscope. After passing a Chinese language login prompt (username and password are blank!), one is greeted by some pages with network-related information such as Wifi and IP settings. But there is nothing related to the actual microscope functionality there. Thus I turned my attention to the Android app.

App analysis

The Rotek website provides an app - aptly named Max-See. I used Jadx to decompile it to Java code. The first finding was that the app supports many types of camera devices with completely different protocols. It differentiates between them by their IP address.

public int F_GetWifiType() {
    // [...]
    String sIP = intToIp(info.getIpAddress());
    String sIP2 = sIP.substring(0, sIP.lastIndexOf("."));
    if (sIP2.equalsIgnoreCase("175.16.10")) {
        this.lianjie = true;
        return 3;
    } else if (sIP2.equalsIgnoreCase("192.168.234")) {
        this.lianjie = true;
        return 0;
    } else if (sIP2.equalsIgnoreCase("192.168.25")) {
        this.lianjie = true;
        return 1;
    // [...]
    } else if (sIP2.equalsIgnoreCase("192.168.29")) {
        this.lianjie = true;
        return 8;

    // [...]
    }
}

Based on the strings in the decompiled code, some of these cameras seem to support RTSP or an HTTP interface for the images. But mine, a "type 8" camera, uses a proprietary UDP-based protocol, as it would turn out.

A native library for ARM, libjh_wifi.so, handles the actual network connection to the microscope. I used Ghidra and its excellent decompiler to analyze this library. Fortunately, it is full of symbols that make it more obvious to understand.

The app calls the naInit() function in the library, which in turn calls naInit_Re(). There, I finally started learning about the protocol, by paying particular attention to the parts of the code that are specific to "type 8" (nICType == 8). The library binds two UDP sockets to ports 20000 ("status service" according to the function name) and 10900 ("data service"), respectively. Then it sends several commands via UDP to port 20000 of the microscope.

cmdbuf._0_4_ = 0x4d43484a; // "JHCM"
cmdbuf._4_2_ = 0x1044;     // "D\x10"
cmdbuf[6] = '\0';
send_cmd_udp(cmdbuf,7,sServerIP,20000);

These commands all begin with "JHCMD", followed by two additional bytes: 0x10, 0x00 in the first command, 0x20, 0x00 in the second command, 0xD0, 0x01 in the final command, which is repeated twice.

Working with Wireshark and decoding data

This sequence of commands I could surely replicate in Python. Looking at the traffic with Wireshark, I saw that the microscope then started flooding me with packets going to port 10900 ‒ the "data service" port that I had previously found. There was a certain periodicity to the packets; every 20 to 30, there was one including the string "GPEnc". "Enc" undoubtedly stands for "encoder", but I could not find out what that string means. However, I noticed something else while looking at these packets: 0xFF, 0xD8, that looked conspicuously like a JPEG start of image marker. Could these be JPEG images with an 8-byte header?

Further analyzing the libjh_wifi.so library for code executed for a "type 8" camera, I found the doReceive_rtp() function, which indeed allocates several jpgbuffers and copies everything received on port 10900 into those except for the first 8 bytes of each packet. Therefore, I exported the UDP packet data from Wireshark and wrote a script to strip the 8-byte headers. And I got this:

JPEGSnoop confirmed that this was indeed a complete JPEG file with 1280 x 720 pixels, even though it's only ca. 25 kByte in size, i.e., heavily compressed. (The image is this blurry because the microscope was out of focus during my testing.)

As for the packet headers: Just going through the packets with Wireshark reveals that the first two bytes are a frame counter, while the fourth byte is a packet counter that resets with every new frame. I didn't decipher the rest of the header yet. Possibly, looking further into libjh_wifi.so would give more clues.

Python prototype

With all this information, I could put together a minimal working example in Python, which you can find on Github. Note that by looking at the naStop() function in the library, I also figured out that command 0xD0, 0x02 stops the stream of packets again. The small Python script dumps frames received from the microscope to disk as JPEG files until you press a key. To detect a keypress, it uses a function from msvcrt; thus, in its current state, it only runs under Windows, unless you modify that particular line.

I can conceive writing a minimalistic GUI instead, to have a live view of the images.

Further findings

What about the "status service" port? Pressing a key (take a snapshot, zoom in, zoom out) on the microscope sends a "JHCMD" packet from the microscope to this port on the computer. I haven't bothered to react to these packets yet.

Further commands for the microscope can be found inside libjh_wifi.so with Ghidra. Maybe, I will explore them in the future.

In the end, due to the substantial JPEG compression, the image quality over Wifi is worse than over the USB cable. Nevertheless, it was an exciting reverse-engineering project.

UPDATE July 2020: On GitHub, user raenye pointed me to two interesting repositories. This repo contains Java source code for an application similar to the Max-See app that I reverse-engineered above. Another repo contains Objective C source code for the iOS counterpart to libjh_wifi.so.