1<!DOCTYPE html> 2<html lang="en"> 3 <head> 4 <meta charset="UTF-8" /> 5 <meta http-equiv="X-UA-Compatible" content="IE=edge" /> 6 <meta name="viewport" content="width=device-width, initial-scale=1.0" /> 7 <title>xor.dev | How I reverse-engineered my Haylou Smart Watch 2</title> 8 9 <link rel="stylesheet" href="/assets/css/post.css" /> 10 <link rel="stylesheet" href="/assets/css/syntax.css" /> 11 12 <link rel="stylesheet" href="/assets/css/common.css" /> 13
13<script src="/assets/js/categories.js"></script>
13 14
14<script> 15 const currentTheme = localStorage.getItem("theme"); 16 if (currentTheme) { 17 document.documentElement.setAttribute( 18 "data-theme", 19 localStorage.getItem("theme") 20 ); 21 } 22 </script>
22 23 24
24<script defer src="/assets/js/lbox.js"></script>
24 25 26</head> 27 28 <body> 29 <main> 30 <header> 31 <a class="site-title" href="/">xor.dev</a> 32 <!-- dark/light mode --> 33 <span id="dark-mode-toggle" style="cursor: pointer"> 34 <svg 35 stroke="currentColor" 36 fill="currentColor" 37 stroke-width="0" 38 viewBox="0 0 16 16" 39 height="1.2em" 40 width="1.2em" 41 xmlns="http://www.w3.org/2000/svg" 42 > 43 <path 44 d="M8 1a7 7 0 1 0 0 14A7 7 0 0 0 8 1zm0 13V2a6 6 0 1 1 0 12z" 45 ></path> 46 </svg> 47 </span> 48</header> 49 50 <section class="article"> 51 <div class="article-header"> 52 <h2 class="article-title">How I reverse-engineered my Haylou Smart Watch 2</h2> 53 <small class="date">24 Jan 2025</small> 54 <div class="categories"> 55 56 <a href="#!" class="category">project</a> 57 58 <a href="#!" class="category">re</a> 59 60 </div> 61 </div> 62 <div class="content"><p>A few years ago I bought a <em>Haylou Smart Watch 2</em>, my first smartwatch. It got to be quite useful for tracking my heart rate and sleeping times, but I quickly started wondering⦠how does it actually work? My curiosity immediately kicked in, and I spent quite some time reversing its functionality and compiling all findings in <a href="https://github.com/XorTroll/ls02-re" target="_blank">this repository</a>.</p> 63 64<p>This post briefly covers the journey of reverse-engineering this smartwatch. Enjoy it ;)</p> 65 66<ul> 67 <li><a href="#reverse-engineering-a-smartwatch">Reverse-engineering a smartwatch</a></li> 68 <li><a href="#surfing-through-bluetooth-gatt">Surfing through Bluetooth GATT</a></li> 69</ul> 70 71<h2 id="reverse-engineering-a-smartwatch">Reverse-engineering a smartwatch</h2> 72 73<p>As you may expect, reverse-engineering such a smartwatch is far from trivial. I would need some sort of vulnerability or exploit just to have some chance to dump the watchâs firmware, which felt way beyond my abilities. This is why, from the start, my goal was to investigate its Bluetooth functionality, something less far fetched but more reasonable for my abilities, still a fun challenge to get into.</p> 74 75<p>This smartwatch has an <a href="https://play.google.com/store/apps/details?id=com.liesheng.haylou" target="_blank">official Android application</a>, a truly terrible one. I couldnât even get to register in the application without it crashing, and I was not the only one experiencing that: the applicationâs Play Store reviews were filled with users complaining about similar issuesâ¦</p> 76 77<p>I searched for another application to control the watch, and soon enough I found <a href="https://play.google.com/store/apps/details?id=hu.tiborsosdevs.haylou.hello" target="_blank">Hello Haylou</a>. Although it explicitly gatekeeps certain functionalities for the premium version, it worked fine. The app even supports around 10 different Haylou smartwatch models, but this RE-ing project is exclusively focused on the <code class="language-plaintext highlighter-rouge">LS02</code> model, the internal model name for the <em>Haylou Smart Watch 2</em>.</p> 78 79<p>I decompiled the Java source (for what I like to simply use <a href="http://www.javadecompilers.com/apk">this great online decompiler</a>) for both applications and started inspecting them, only to find that the official application was even worse than I thought. Most of the code was not in Java but in native libraries - which I could just pop into IDA and try to get something out of them - but then it got even worse: said native code was doing some sort of emulation of a blob of⦠maybe PPC code it had embedded? God, why so much chaos for obfuscating such a broken app?</p> 80 81<h2 id="surfing-through-bluetooth-gatt">Surfing through Bluetooth GATT</h2> 82 83<p>Luckily for me, the unofficial app was far less chaotic and more reasonable to be inspected, although it still contained obfuscated package and class names, like release Android apps typically have. From this point, it was a matter of cleverly finding any Bluetooth-related code.</p> 84 85<p>I started looking for any classes which imported Android Bluetooth classes/types. This eventually led me to two relevant classes, <code class="language-plaintext highlighter-rouge"><obfuscated-terms>.tiborsosdevs.tibowa.MiBandSupport</code> and <code class="language-plaintext highlighter-rouge"><obfuscated-terms>.tiborsosdevs.tibowa.LS02WatchHandler</code> (yeah, the class names were surprisingly not obfuscated).</p> 86 87<p>This and many other smartwatches work using Bluetooth GATT. They provide a list of services via Bluetoothâs GATT protocol, where each service contains a list of so-called âcharacteristicsâ. Think of these as communication channels between the phone/device and the smartwatch. Both services and characteristics are identified by UUIDs, and luckily I was able to list all the ones provided by the smartwatch when I got to test my findings. After connecting through one of these characteristics, the smartwatch may
87send raw data bytes to the device, and vice versa, depending on the characteristic type.</p> 88 89<blockquote> 90 <p>There are a few different GATT characteristic types: <em>Indicate</em>, <em>Notify</em>, <em>Read</em>, <em>Write</em>, <em>WriteWithoutResponse</em>⦠for the LS02 smartwatch, the relevant characteristics are <em>Read</em>+<em>Write</em>, <em>Read</em>+<em>WriteWithoutResponse</em> or <em>Notify</em>.</p> 91</blockquote> 92 93<p>The <code class="language-plaintext highlighter-rouge">LS02WatchHandler</code> class contained the UUIDs of the relevant characteristics used by the smartwatch as static strings. Moreover, by navigating through some interesting classes which were referencing this class, it was a matter of time until I found some byte arrays which seemed to be the plain data sent via GATT characteristics:</p> 94 95<p><img src="/assets/posts/ls02/code.png" alt="code" /></p> 96 97<p>After tracking their usage, finding the code actually dispatching raw data (which annoyingly was not properly disassembled, yet I was able to understand it through the Java bytecode output) and putting a quick code to test it, I was indeed sending and receiving data from the smartwatch.</p> 98 99<h1 id="device--smartwatch-communication">Device â smartwatch communication</h1> 100 101<p>I performed my initial tests in a custom Android app, almost copying their Bluetooth setup. It didnât take me long to notice that I could just be using a PC program for testing, instead of annoyingly inspecting output logs of my Android app. I conveniently picked Rust for this task, whose crate system simplified a lot having to deal with Bluetooth library dependencies. Keep in mind that I had never worked with Bluetooth on a dev/technical level prior to these experiments, so I was slowly digesting the functionality and concepts of the GATT protocol while I was starting to find promising stuff on the reverse-engineered Java code. I had never touched any kind of Bluetooth libraries for development before.</p> 102 103<p>My first tests, of course, were trying to send the simplest possible commands. The first step was to pair the device, in order for the smartwatch to recognize me. As can be seen in the screenshot of the REâd Java code above, pairing seemed to be performed by sending a two-byte command <code class="language-plaintext highlighter-rouge">{0x20, 0x02}</code>. This was likely the first command I tried, and the result probably sparked a âeurekaâ moment in me: after the pairing is performed (which just requires sending that command), the smartwatch begins sending a lot of commands periodically with watch information. 104Ironically, I was actually sending a badly formatted command. The command should be of the form <code class="language-plaintext highlighter-rouge">{0x20, 0x02, <pair-key>}</code>, where the pair key consists of 4 extra bytes of the unique key characterizing the pairing. By sending a smaller data array, these expected 4 bytes were treated as <code class="language-plaintext highlighter-rouge">0xFF</code> by the watch by default, so I was essentially pairing with a <code class="language-plaintext highlighter-rouge">(0xFF, 0xFF, 0xFF, 0xFF)</code> key. This was still a successful pair, since the command seemed to work fine. The reversed code probably does it correctly by appending some key after the base command, but I havenât really looked into it.</p> 105 106<p>What about the other commands? For instance, one simple type of command contains the current battery level: it consists of a two-byte command <code class="language-plaintext highlighter-rouge">{0xA2, <battery-level>}</code> where the battery level value is the battery percentage sent as a byte (for instance, a 69% level would be indicated when receiving <code class="language-plaintext highlighter-rouge">{0xA2, 0x45}</code> from the watch). This command is always sent to the device after pairing, and is also sent each time the battery percentage changes. Other commands are also sent periodically without prior request, such as the recorded step count (distinguishing between steps done walking and running), heart rate and so on.</p> 107 108<blockquote> 109 <p>There are quite a lot of commands used by the smartwatch, where only a handful of them are briefly explained in this post for clarity/as a few examples. A detailed list of all reversed commands, along with basically everything Iâve reverse-engineered about this smartwatch, can be found <a href="https://github.com/XorTroll/ls02-re/blob/main/RE.md" target="_blank">in my RE docs</a>.</p> 110</blockquote> 111 112<p>One of the most satisfying commands to document were notification commands. These are used by the device (aka the Android app connected to the smartwatch) to notify the smartwatch when a certain app has notifications on the smartphone, like WhatsApp or Facebook messages. This is only used for messages and email, since phone calls are handled with different c
112ommands. 113These commands work in bulk, due to limitations in the data sizes that can be sent at once using GATT. The text message of the notification is split in chunks of max. 22 characters (although I recall finding that the reversed code uses a slightly smaller chunk size for some reason) and multiple commands are sent with the message pieces, starting with an initial command and ending with a final command to indicate start and finish of the long command sequence respectively.</p> 114 115<p><img src="/assets/posts/ls02/testing.jpg" alt="testing" /></p> 116 117<p>Other commands use a similar technique of splitting their content into multiple commands as well, such as when retrieving the heart rate registry (the results of the last hours are sent in separate chunks).</p> 118 119<h2 id="experimenting-with-watch-commands">Experimenting with watch commands</h2> 120 121<p>After I felt I had tested a good amount of the commands found in the reversed code, I was wondering if there were any commands that these non-official devs didnât even know. By this point, I switched from testing commands found in their code to sending experimental command combinations, extra bytes, weird values⦠just to see how the watch reacted. This is, in fact, how I found out about the pairing key aspect mentioned above. By sheer trial and error I was able to get a better understanding of the command parameters than what the reversed code was doing. After all, this is way more fun than having to try to understand obfuscated and sometimes not even correctly disassembled Java code. This is how the largest part of <a href="https://github.com/XorTroll/ls02-re/blob/main/RE.md" target="_blank">my reverse-engineering docs</a> were filled, since playing with commands and even with previously untouched characteristics led me to discover new weird commands being periodically sent through those alternative channels.</p> 122 123<h2 id="current-state">Current state</h2> 124 125<p>I eventually got a new Xiaomi smartwatch not so long ago, which partially explains the decline of motivation towards this project. Moreover, since this is not a well-known smartwatch (letâs be honest, itâs a cheap smartwatch from some random Chinese manufacturer) there is not such a big interest in continuing the work for others who might find it useful.</p> 126 127<p>I have tried doing the same with the new Xiaomi smartwatch, but i havenât been able to find anything. Commands appear to be either compressed or encrypted, and the disassembled Java code is not as easy to navigate as the one from Hello Haylou.</p> 128</div> 129 </section> 130 <footer> 131 <p>© 2026 | XorTroll</p> 132</footer>
133<script src="/assets/js/mode.js" defer></script>
133 134 135<!-- GoatCounter -->
136<script data-goatcounter="https://xortroll.goatcounter.com/count" 137 async src="//gc.zgo.at/count.js"> 138</script>
138 139 140<!-- Keep Android Open banner -->
141<script src="https://keepandroidopen.org/banner.js"></script>
141 142 143 </main> 144 <section id="category-modal-bg"></section> 145<section id="category-modal"> 146 <h1 id="category-modal-title"></h1> 147 <section id="category-modal-content"></section> 148</section> 149 150 </body> 151</html>
Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.