This article also has a Chinese version.
Let’s dig out some past work. These were some projects I worked on in July last year, and some of them might have become obsolete by now.
Last summer, I stumbled upon this device called the MiaoMiao Printer. As someone who enjoys tinkering, I got involved with reverse engineering and developing related to the MiaoMiao Printer. All the resources are available at: https://github.com/ihciah/miaomiaoji-tool
The MiaoMiao Printer is a Bluetooth thermal printer that you can connect to and print from using its app via Bluetooth. I once saw on Weibo that DIYGOD had fun with a remote print push project, which inspired me to create something similar. However, I did not fancy the idea of constantly needing to operate it manually from my phone. Considering I had a Raspberry Pi 3 handy, with Bluetooth functionality, I decided to take apart the MiaoMiao Printer’s app, extract its communication protocol, and then set up a control script on the Raspberry Pi.
App Reverse Engineering
Originally, I was too lazy to take apart the app and thought I might just snoop the Bluetooth logs and make an educated guess, but obviously, that didn’t work.
So, I went ahead and decompiled the Apk. I downloaded the latest version of the app along with several older versions and found they were all fortified with 360 protection, which was annoying. Eventually, after some effort with VM unshelling, I was able to dump the dex file while the system was loading it. It is a common tactic since current reinforcements are quite similar to PC application shells from years ago—they dynamically decrypt the code and execute it in memory when loaded. All you need to do is find the Original Entry Point (OEP) and then dump the code. In Android, dumping the dex upon loading will reveal the content of the reinforcement.
Apart from dex, the app also contained a bit of native code. By dragging it into IDA, I was able to obtain a key, which I will discuss later.
Bluetooth Communication
After examining plenty of (naturally obfuscated) garbage code, I was able to reverse engineer the communication protocol:
1 | (1 byte) 2 |
When the payload is too long, it is split into multiple packets with a maximum length of 2016
and sent sequentially.
The CRC32
checksum starts with an initial value of crcKey = 0x35769521
. Control commands include PRT_PRINT_DATA
and others, with numeric constants; there are 26 in version 1.0.2
of the app, which increased to 47 in version 2.0.3
, whilst maintaining backward compatibility.
The messages returned by the machine follow the same protocol. When the command field of the last packet is \x00
, it signifies the conclusion of the message transmission.
At this point, we just need a language such as Python to replicate the protocol.
Here I have implemented a picture printing interface, wherein images received at the backend of a WeChat official account are automatically sent to the Raspberry Pi to be printed.
WeChat Side | MiaoMiao Printer Side (microUSB strictly for charging) |
---|---|
Additional tinkering examples:
Different depths of test print pages | Failed examples |
---|---|
Server Communication
Request packet:
1 | POST /api/ap/Login HTTP/1.1 |
Where:
ts
is the integer part oftime.time()
sign
is the lowercase MD5 of the stringuserId=xxx&msg=xxx&ts=xxx&signKey=ab69a9d9-94a5-4111-9554-00af2917732f
msg
in the signed string is a json string:{"ip":"","language":"CN","remark":"{\"brand\":\"samsung\",\"device\":\"SMA3000\",\" os\":\"and\",\"release\":\"5.0.2\",\"baseObjId\":0}","userName":"ihciah@gmail.com","userPassWord":"md5_of_password","type":1,"
- Within the body,
msg
is the encrypted version ofmsg_original
(the above json string) as a json string (should be encoded withurllib.quote
, although it seems not necessary to quote based onios
app packet capture?) - Original message:
{"swType":"mmj_p1_fw"}
Odd AES encryption:QpJ42KMTg1f9msKsG5Xz3JY5nGL0UVQWZtAcFRG5dA8=
Formed into a json string:{"parameter":"QpJ42KMTg1f9msKsG5Xz3JY5nGL0UVQWZtAcFRG5dA8\\u003d\\n","baseObjId":0}
Encoded:%7B%22parameter%22%3A%22QpJ42KMTg1f9msKsG5Xz3JY5nGL0UVQWZtAcFRG5dA8%5Cu003d%5Cn%22%2C%22baseObjId%22%3A0%7D
- The
AES
encryption algorithm usesAES_ECB
, and reversinglibalf_h_sdkcore.so
yields the key:f3e15c3a845d48dc
(also found in theclasses.dex
file), with output encoded usingb64encode
, with a special point that all+
characters should be replaced with-
The response packet consists of a json string {"data":"xxxxx"}
, where the data value also adheres to the aforementioned odd AES encryption. The reverse operation will give you unencrypted json responses.
1 | from Crypto.Cipher import AES |
Server Side
The mobile app uses the Aliyun OSS SDK to load images and other resources, strangely with no appearance in Burp. It likely does not go through the system HTTP proxy, so I turned to Wireshark. The captures showed that these requests all use files within BUCKETID mb-mm
, which is a private BUCKET requiring OSSAccessKeyId
and Signature
. Normally, this signature should be completed at the server side, but attempts to download fonts in iOS did not return any signature from the server. Hence, it is determined that the signing is done within the app. Examining the app’s related code and Aliyun OSS’s Android SDK demonstrated that you can set AccessKeyId
and AccessKeySecret
through OSSPlainTextAKSKCredentialProvider
. Therefore, we obtained:
1 | AccessKeyId: LTAIrPvoTOwid4QD |
After decrypting the captured packets, I found the name of the latest firmware: mmj_p1_fw_v127.bin
. Planning to attempt a download from OSS, I instead directly downloaded all historical versions of APKs and firmwares. Lol.
That is to say, if we managed to produce and replace firmware conforming to the signature, we could potentially brick all MiaoMiao Printers upon updating the firmware. Or alternatively, inject some backdoor code. Moreover, that OSS contains all users’ remotely printed images. If you explore carefully, you will discover some peculiar images (facepalm).
Moreover, during the testing process, I stumbled upon some injection vulnerabilities. For example:
1 | POST /AjaxSub/GetSubscriptionCategory |
We can directly extract the table names:
1 | Database: mm_subscription_db |
Hardware
After much experimentation, I was unable to analyze the firmware format or entry points. However, while examining the hex data, I noticed the term YC1021
. Googling revealed that this referred to a Bluetooth chip. The next day, dismantling the machine revealed the manufacturer Nuvoton
, chip model NUC123LD4BN0
, with the processor chip being STM32F071CBU6
of the Cortex M0
architecture.
Loading it into IDA as ARM6M format suggested it might be comprehensible. Searching for the immediate value of 0x180, which corresponds to 384 in decimal—a count of pixels per line—I found:
1 | SRAM_BASE = 0x20000000 |
Firmware modifications can be uploaded in two ways: one is ISP
supported by Nuvoton
, implemented by the firmware itself as an interface called post-Bluetooth upload, and the other is through USB
. Considering that a wrong Bluetooth update might be irreversible, I first made sure the USB firmware update method worked.
I headed to the official website, only to hit a snag again as a programmer was needed to flash it…
I had planned to modify the firmware to allow the machine to process grayscale images and to re-ingest the paper it prints, thus enabling prints with varying shades through multiple printings. But alas, the project was shelved. If any technical wizards are willing to take this on, feel free to give it a try.
Additionally, a chap who had once reached out to me about reverse engineering the MiaoMiao Printer a year ago is now… erm, working for this company. Quite a fascinating turn of events.