Next Stop - Ihcblog!

Some creations and thoughts sharing | sub site:ihc.im

0%

Memoirs of Tinkering with the MiaoMiao Printer

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
2
3
4
5
6
7
(1 byte) 2
(1 byte) Control Command
(1 byte) Current payload packet serial number
(2 byte) Payload Length
(n byte) Payload
(4 byte) CRC32 checksum of the payload
(1 byte) 3

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
2
3
4
5
6
7
8
9
10
11
12
13
POST /api/ap/Login HTTP/1.1
ts: 1502784192
sign: b71a3f80dab036e3110532590972961e
Accept-Encoding: gzip, deflate
Connection: close
Accept: **/*//*
userId: 1758717881
language: zh_CN
User-Agent: paperang_mm_android
Content-Type: application/x-www-form-urlencoded
Content-Length: 442
Host: ifs.mm.paperang.cn
msg=xxxxxxx

Where:

  • ts is the integer part of time.time()
  • sign is the lowercase MD5 of the string userId=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 of msg_original (the above json string) as a json string (should be encoded with urllib.quote, although it seems not necessary to quote based on ios 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 uses AES_ECB, and reversing libalf_h_sdkcore.so yields the key: f3e15c3a845d48dc (also found in the classes.dex file), with output encoded using b64encode, 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
2
3
4
5
6
from Crypto.Cipher import AES
import base64
def decrypt(msg):
msg = base64.b64decode(msg.replace("-", "+"))
c = AES.new("f3e15c3a845d48dc", AES.MODE_ECB)
return c.decrypt(msg)

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
2
AccessKeyId: LTAIrPvoTOwid4QD
AccessKeySecret: 5xxJHjKmgMEFaqXhb3VZ2QrkcFRWde

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
2
POST /AjaxSub/GetSubscriptionCategory
typeId=2-1

We can directly extract the table names:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Database: mm_subscription_db
t_examineimagecache - 4829
t_material - 30459
t_materialcollectionconfig - 550402
t_materialgroup - 717
t_materialgroupconfig - 23593
t_materialoperationrecords - 3866621
t_message - 0
t_subcontentoperationrecords - 214243
t_subscription - 507
t_subscriptioncategory - 10
t_subscriptioncheck - 901
t_subscriptioncontent - 5642
t_subscriptionmaterial - 115
t_subscriptionrecord - 505050
t_subscriptionuserconfig - 396411
t_subscriptionusergroup - 6

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.

Welcome to my other publishing channels