1. Overview
In Part 1 of the series, we looked at trends in the robotics industry and in vulnerability research, and we summarized the exterior, hardware chipsets, and the main features offered by the smartphone app of the Unitree Go2 Pro that we chose as our research target.
However, the product specifications and app screens alone do not tell us what software actually runs inside the robot. To understand which process receives the command when you press the “Sit” button in the app, which service sends the camera feed to the outside, and in what order the dozens of features come up after powering on, we need to analyze the robot’s internal filesystem and its running processes.
In Part #2, we dump the contents of the storage device, build a dynamic analysis environment, and then cover our analysis of the system’s early boot process. We start by finding the hardware UART, obtain a shell using a 1-Day-based tool, extract the eMMC and the OTA firmware, analyze the firmware structure, and finally build a debugging environment on the latest firmware version.
2. Identifying the Hardware Debugging Interface
2.1. Examining the Internal Structure of the Target Hardware
One of the first things to look at when analyzing embedded equipment is the physical interfaces left on the board. Debugging ports used during manufacturing and expansion ports for developers are often left in place even in mass-produced products. These interfaces are necessary for normal maintenance and production testing, but without separate access controls they can become an entry point that leads to the operating system’s boot log or an administrator shell.
Go2 top-cover removal location from Unitree’s official documentation
We first checked the top-cover fastening screws and the cover removal location in Unitree’s official documentation.[1]
Connectors around the Go2 internal mainboard
As we confirmed in Part #1, several connectors are present, and checking the board silkscreen around the connectors, we could see characters such as RX/TX and CORE_UART etched onto it.
We presumed these connectors to be UART-related pins, so we purchased a separate connector, connected it to a Logic Analyzer, and worked to confirm whether the connector really carried UART communication.
UART (Universal Asynchronous Receiver/Transmitter) is a communication method that exchanges serial data over two lines, TX and RX, without a separate clock line. A UART port usually consists of three lines, GND, TX, and RX, so candidate pins are easy to find on the board, but you cannot be sure a connector is a UART port just from its shape. You therefore have to verify, in turn, the pin arrangement, the signal voltage, the communication speed, and the shape of the output data.
2.2. Identifying and Verifying the Candidate UART Interface
Based on the board silkscreen and the connector structure, we presumed the connector labeled CORE_UART and RX/TX/GND to be a UART port candidate. After connecting the connector to the Logic Analyzer, we captured the pin signals. If a steady, uninterrupted pulse appears when power is turned on, there is a high chance that a boot log is being output.
Capture Raw UART data with the Logic Analyzer
UART data decoded as ASCII at 1.5Mbps
As confirmed with the Logic Analyzer, the Go2’s main console operated at 1.5Mbps. Based on the bit width of the captured signal, we substituted various baud rates and found the value at which the character corruption disappeared. Once we set it to 1.5Mbps, the boot logs of U-Boot and the Linux kernel were printed as proper strings. This meant the CORE_UART port was not a simple sensor UART but the serial console of the main operating system.
State data observed on the basic_service serial line
For the connector presumed to be another UART port, the output differed from what we had seen on the CORE_UART console. Here, robot-related state data such as pitch and position streamed out continuously. During analysis, both an approximately 300000bps setting and a 921600bps capture were recorded.
UART ports identified on the Go2 mainboard
Category | Baud Rate | Role |
Main UART | 1.5Mbps | U-Boot/kernel boot log and Linux login console |
Robot state data transmission | Centered on 300000bps, 921600bps capture recorded | Low-level state data stream such as pitch and position |
Yellow was identified as the robot-related state data, and red as the main UART console. The two ports were understood to serve, respectively, as the Linux login console and as the state stream of the robot control service.
system login confirmed on the UART console
On the main UART, after booting finishes, you can see a Linux-based login prompt.
3. Acquiring and Extracting the Target Firmware
3.1. Identifying the Target Device and Firmware Version
The very first thing to do before extracting firmware is to pin down the target version. Even for the same product, the executables, security settings, and whether certain services and vulnerabilities apply all differ by firmware version. As confirmed in Part #1, the firmware version of the Go2 Pro we purchased is v1.1.11.
Robot version confirmed in the Unitree Go app
•
Hardware : v2.0
•
Software : v1.1.11
The v1.1.11 firmware was a good starting point for beginning this research. At the time, an open-source tool that enables the administrator shell on that version was publicly available[2], and it allowed us to dump the eMMC from the running system without physically removing the storage device.
3.2. Obtaining a Shell with the UnLeash-Lite Tool
UnLeash-Lite is a tool that can perform tasks such as enabling SSH on robots that have a 1-Day vulnerability. The v1.1.11 Go2 firmware can have its SSH feature enabled using this tool.
The specific vulnerability trigger conditions and payload composition are implemented inside the tool, and the detailed attack logic can be found in the UnLeash-Lite GitHub code.
Unleash-Lite SSH enablement settings
First, we switched the robot to AP mode and connected the analysis PC to the robot’s WiFi. In this mode, the robot uses 192.168.12.1 and the PC receives an address in the 192.168.12.0/24 range. Then we brought up the UnLeash-Lite dashboard, specified the device settings, and applied the SSH enablement feature.
unleash-lite serve
PowerShell
복사
SSH enablement trigger log
When we ran the tool, the SSH service was enabled on the robot’s port 22, and we could access the shell with the root account created by Unleash-Lite.
root privilege SSH shell obtained after execute UnLeash-Lite
root@Unitree:~# id
uid=0(root) gid=0(root) groups=0(root)
root@Unitree:~# uname -a
Linux Unitree 5.10.176-rt86+ #74 SMP PREEMPT_RT \
Wed Oct 16 15:26:07 CST 2024 aarch64 GNU/Linux
root@Unitree:~# grep PRETTY_NAME /etc/os-release
PRETTY_NAME="Ubuntu 20.04.5 LTS (Focal Fossa)"
Bash
복사
Right after obtaining the shell, we first checked the system with basic commands such as uname, /etc/os-release, lsblk, mount, ps, and systemctl. The Go2’s user space was Ubuntu 20.04.5 LTS, the kernel was Linux 5.10.176-rt86+ PREEMPT_RT, and the architecture was aarch64.
3.3. Acquiring the Root Filesystem and the eMMC Image
3.3.1. Obtaining Partition Information via the Shell
eMMC disk and 8 partitions confirmed in the shell
In the lsblk output, we confirmed a 58.3GB mmcblk0, partitions p1~p8, and two hardware boot areas.
root@Unitree:~# lsblk -o NAME,SIZE,TYPE,MOUNTPOINT
NAME SIZE TYPE MOUNTPOINT
mmcblk0 58.3G disk
├─mmcblk0p1 4M part
├─mmcblk0p2 4M part
├─mmcblk0p3 2M part
├─mmcblk0p4 64M part
├─mmcblk0p5 128M part
├─mmcblk0p6 32M part
├─mmcblk0p7 42.5G part /
└─mmcblk0p8 15.5G part
mmcblk0boot0 4M disk
mmcblk0boot1 4M disk
root@Unitree:~# blkid /dev/mmcblk0p{1..8}
/dev/mmcblk0p1: PARTLABEL="uboot"
/dev/mmcblk0p2: PARTLABEL="misc"
/dev/mmcblk0p3: PARTLABEL="uni"
/dev/mmcblk0p4: PARTLABEL="boot"
/dev/mmcblk0p5: PARTLABEL="recovery"
/dev/mmcblk0p6: PARTLABEL="backup"
/dev/mmcblk0p7: LABEL="linuxroot" TYPE="ext4"
/dev/mmcblk0p8: LABEL="uni-backup" TYPE="ext4"
Bash
복사
3.3.2. Dumping the Physical Storage Device with dd
To analyze the static firmware structure, we need the raw bytes of the disk. So we used dd to extract, as separate images, the entire eMMC user area, the individual partitions, and the eMMC hardware boot areas. Since we could not bring the images out via a separate USB mount or similar, we extracted them by streaming the bytes read on the robot to SSH standard output and redirecting them into image files on the analysis PC.
# Entire eMMC user area
ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
root@192.168.12.1 \
"dd if=/dev/mmcblk0 bs=4M iflag=fullblock status=progress" \
> unitree_mmcblk0_full_disk.img
# Example of a per-partition dump: p1
ssh -o ServerAliveInterval=30 -o ServerAliveCountMax=10 \
root@192.168.12.1 \
"dd if=/dev/mmcblk0p1 bs=4M iflag=fullblock status=progress" \
> unitree_mmcblk0p1_uboot.img
Bash
복사
Since the full image was about 59GB, we also applied keep-alive options so that the SSH session would not be dropped midway.
Disk and partition images saved on the analysis PC
Through the method above, we obtained 3 disk images, mmcblk0, mmcblk0boot0, and mmcblk0boot1, and 8 partition images from p1 to p8.
Image | Size | Initial identification result |
unitree_mmcblk0_full_disk.img | 59GB | Entire eMMC user area |
unitree_mmcblk0boot0_disk.img | 4MB | eMMC HW boot0, empty |
unitree_mmcblk0boot1_disk.img | 4MB | eMMC HW boot1, empty |
unitree_mmcblk0p1_uboot.img | 4MB | U-Boot/ATF/OP-TEE |
unitree_mmcblk0p2_misc.img | 4MB | Security firmware snapshot |
unitree_mmcblk0p3_uni.img | 2MB | Factory identity information |
unitree_mmcblk0p4_boot.img | 64MB | PREEMPT_RT Linux kernel |
unitree_mmcblk0p5_recovery.img | 128MB | Recovery image |
unitree_mmcblk0p6_backup.img | 32MB | Reserved area, empty |
unitree_mmcblk0p7_linuxroot.img | 43GB | ext4 main root filesystem |
unitree_mmcblk0p8_uni-backup.img | 16GB | Factory reset backup |
4. Analyzing the eMMC Firmware Structure
4.1. eMMC Partition Layout
Go2 eMMC partition labels and UUIDs confirmed with blkid
When we checked the partitions with the blkid command in the shell and compared them against the dumped images, the Go2’s 64GB eMMC user area was divided into 8 partitions. The displayed capacity is about 58.3GB, and the running Linux’s / is mounted on mmcblk0p7.
mmcblk0
├─ p1 uboot 4MB U-Boot FIT + ATF/OP-TEE
├─ p2 misc 4MB ATF/OP-TEE related snapshot
├─ p3 uni 2MB Factory identity record
├─ p4 boot 64MB PREEMPT_RT kernel FIT
├─ p5 recovery 128MB Android-family recovery image
├─ p6 backup 32MB Empty reserved area
├─ p7 linuxroot 43GB ext4 main root filesystem
└─ p8 uni-backup 16GB Factory reset backup
Plain Text
복사
This structure differed from the common update scheme, like on a smartphone, where entire A/B slots are used alternately.
The bootloader, kernel, main rootfs, recovery image, and factory reset backup each held their own role, and most of the running operating system and Unitree services were contained in p7.
4.2. Analysis of Each eMMC Partition
4.2.1. Bootloader and Real-Time Kernel Partitions
p1 was a U-Boot FIT (Flattened Image Tree) container used by Rockchip-family embedded Linux. A single FIT contained not only the U-Boot body itself but also the ARM Trusted Firmware BL31, OP-TEE, and the device tree together.
Booting roughly starts from the ROM and the initial loader, then proceeds in the order ATF, OP-TEE, U-Boot, and the Linux kernel.
A FIT is a format that bundles multiple binaries into a single tree and manages the hash and signature of each image. In the Go2’s p1 as well, a signature configuration based on SHA-256 and RSA-2048/PSS was confirmed.
p4 is the Linux kernel image that U-Boot loads. The kernel confirmed at runtime was 5.10.176-rt86+, with the PREEMPT_RT patch applied. In robots, where the predictability of latency matters more than in a typical server, the motor control loop must run on a fixed cycle, so a real-time kernel is needed. The boot options also included isolcpus=3, suggesting a configuration that aims to dedicate a specific CPU core to real-time tasks.
4.2.2. The Ubuntu Root Filesystem and the Unitree Service Area
The p7 partition is linuxroot. It is in ext4 format and is mounted as / at runtime. Here, along with the user space of Ubuntu 20.04, the Unitree-specific software was gathered under the /unitree directory.
/unitree/module Executables and scripts for each robot feature
/unitree/etc Service configuration and policy files
/unitree/var PIDs, Unix sockets, logs, and runtime state
/unitree/ota OTA engine, packages, backups, and update work area
Plain Text
복사
Looking at /unitree/module shows which features are broken out into independent services, and looking at /unitree/etc/master_service shows the start command for each service as well as its conflict and protection policies.
In addition, the PID files in /unitree/var/run show which services are running right now, and /unitree/ota was presumed to be a directory related to pre- and post-update backups and package processing.
The representative services found in p7 include basic_service, sport_mode, motion_switcher, robot_state, videohub, go2_videohub_rtsp_server, webrtc_bridge, webrtc_signal_server, btgatt-server, ota_boxed, chat_go, pet_go, and programming_actuator.
The remote control, video, voice, AI conversation, pet mode, programming, and OTA features that we saw in the app were each composed of their own process and configuration files within the filesystem.
4.2.3. Recovery and Backup Partitions
p3 uni held device identity information recorded at the factory stage. The magic string and part of the serial number appeared to be in plaintext, while the remaining area was an encrypted record.
p5 recovery was an Android 12-family recovery ramdisk structure, and p8 uni-backup was a large backup space for factory reset.
Inside p8 were the rootfs, kernel, boot image, and device tree, serving to restore the device to its shipping state, while the p6 backup area was empty.
5. Analyzing the OTA Update Firmware Structure
If the eMMC dump shows the full picture of the robot at a specific point in time, the OTA package shows what actually changes when the firmware version is raised. First, we analyzed the update delivery path between the app and the robot and obtained the v1.1.15.1 package. We then analyzed the header of the UPK container and the processing logic of ota_utils, and examined the update modules contained inside the package.
5.1. Obtaining the OTA Package
At first, we expected that the smartphone app would download the firmware file directly and send it to the robot. That is because when the user presses Update in the app’s Device menu and watches the progress, it looked as if the app was delivering the update data to the robot.
Continuous OTA Upgrade notice on Unitree’s official page
Unitree’s official material also explains that the firmware is updated through a cloud OTA program[3]. As expected above, to confirm whether the app downloads the update package and delivers the update file to the robot, we analyzed the Android version of the Unitree Go app.
5.1.1. Decrypting the Unitree Go2 App
Download&Install Unitree Go2 App
We downloaded the Unitree Go2 app from the official page, installed it on an Android phone for analysis, and attempted to decompile it to analyze the update process.
Encrypted JavaScript in the Unitree Go app
Vue JavaScript bundle confirmed after decryption
We attempted to decompile the Unitree Go APK file, but it was encrypted and decompilation was not possible. The app had a hybrid structure combining native code and a WebView frontend, and both layers had protections applied that hinder static analysis.
•
The native DEX was protected by a Baidu packer using libbaiduprotect.so
•
The WebView’s Vue-based JavaScript bundle was stored in the APK encrypted with AES
Simply opening the APK with jadx yielded only a loader stub instead of the actual logic. So we extracted the DEX that the packer had restored from the memory of the running app, and then decompiled it again with jadx. And for the WebView assets, we found the AES key and IV hardcoded inside the app and decrypted them with a separate script.
5.1.2. The App’s OTA Update Sequence
POST /v1/firmware/package/upgrade/list
response fields:
version, description, firmwareId, md5, download
POST /v1/firmware/package/download
response:
updateId
Consumer of the download URL:
not the app, but the robot's ota_boxed
Plain Text
복사
Following the OTA-related classes and REST APIs in the decrypted code, we found that the app was not the entity that directly delivers the OTA package. Our analysis showed it was closer to a control function that queries available update versions, instructs the cloud to download and install, and displays the progress.
The POST /v1/firmware/package/upgrade/list response model had a download field along with the version, description, firmwareId, and MD5. This field contains the direct URL of the UPK file, but the app did not download the file from that URL.
The actual firmware/package/download API also did not return a file but returned an updateId pointing to an update task. When the app requested the task from the cloud, the structure was such that the robot’s ota_boxed fetched the actual firmware directly from the CDN.
App selects Update
→ Instructs the Unitree REST API to query the version, download, install
→ The Unitree cloud delivers a per-robot MQTT command
→ The robot's ota_boxed receives the CDN URL
→ ota_utils downloads, verifies, unpacks, installs the UPK
Plain Text
복사
OTA execution sequence among the app, cloud, and robot
5.1.3. Downloading the OTA Package
To confirm whether the robot’s ota_boxed really downloads the update package directly from the CDN when an update is requested, we examined the robot’s OTA log.
v1.1.15.1 CDN package URL confirmed in the ota_boxed log
As a result, in the ota_boxed.LOG log file we could identify the version, CDN URL, package name, and verification values, and through the UPK download path on firmware-cdn.unitree.com we were able to obtain the v1.1.15.1 OTA package.
5.2. UPK Container Structure
The .upk file we obtained would not open even when the extension was changed to TAR or ZIP, and no common archive identification information appeared either. Since the header size and field layout were not known from the start, we first checked for repeating values and strings in the byte array at the front of the file. As a result, we found the UTPK string at the start of the file and the TEA\x00 string further in. However, these two strings alone could not establish the boundaries of each field, the meaning of the values, or the position where the actual data begins.
The OTA log confirmed in Section 4.1.3 contained logs that appeared to show the robot’s ota_utils processing this UPK directly. Accordingly, we attempted to analyze the UPK package file structure by reverse engineering the ota_utils binary.
5.2.1. Identify UPK Package Header
// ota_utils::Package::UnpackFile wrapper: sub_A115F0
_QWORD *__usercall sub_A115F0@<X0>(
_BYTE *a1@<X0>, __int64 a2@<X1>, _QWORD *a3@<X8>)
{
...
if ( (unsigned __int8)sub_A129A8(a1, a1 + 136, v54) != 1 )
...; // CheckSign(payload MD5)
if ( (unsigned __int8)sub_A12750(a1) != 1 )
...; // IsTar
if ( (unsigned __int8)sub_A127A0(a1) ) // IsTea
{
v55[0] = 0;
v55[1] = 0;
v28 = sub_A1281C(a1); // GetSeed
sub_A132F8(v28, v55, 16); // GenerateCodeKey
sub_A18A0C(v44, v49, v55); // Tea::DecodeFile
sub_9F277C(v44); // delete .TMP
}
else
{
sub_9F27A4(v44, v49); // rename if not encrypted
}
sub_A16408(v49, a1 + 8); // Tar::ExtractAll
...
return a3;
}
JavaScript
복사
The above is the result of decompiling the Package::UnpackFile wrapper portion of ota_utils. It verifies the signature value via the CheckSign function (an MD5 comparison), checks the TAR flag, and then, if the TEA flag is set, generates a key from the seed to decrypt the .TMP into a .TAR. The last call extracts the decrypted TAR into the output directory.
// ota_utils
bool __fastcall Package_IsPackage(_BYTE *self)
{
return self[0x68] == 'U'
&& self[0x69] == 'T'
&& self[0x6A] == 'P'
&& self[0x6B] == 'K';
}
__int64 Package_GetSize(__int64 self)
{ return *(_QWORD *)(self + 0x78); }
bool Package_IsTar(__int64 self)
{ return (*(_DWORD *)(self + 0x80) & 1) != 0; }
bool Package_IsTea(__int64 self)
{ return (*(_DWORD *)(self + 0x80) & 2) != 0; }
__int64 Package_GetSeed(__int64 self)
{ return *(unsigned int *)(self + 0x84); }
JavaScript
복사
Decompiling the functions called by the Package::UnpackFile function and the package-related functions, we confirmed code that checks the UTPK magic value at this+0x68 of the object. Package::GetSize read the 8-byte size value at this+0x78, Package::IsTar read the bit flags at this+0x80, and Package::GetSeed read the 4-byte value at this+0x84.
Recalculating relative to the position where the header begins in the Package object, the value read by GetSize corresponded to header offset 0x10 of the UPK package, the flags to 0x18, and the seed to 0x1c.
// ota_utils::Package::GetName
__int64 __usercall sub_A12904(__int64 a1, __int64 a2)
{
*(_BYTE *)(a1 + 215) = 0; // this+0xD7
v3 = a1 + 152; // this+0x98 -> 0x98 - 0x68 == 0x30
std::string::basic_string(a2, v3, ...);
return a2;
}
JavaScript
복사
// ota_utils::Package::CheckSign
// Call from UnpackFile() -> sub_A129A8(a1, a1 + 136, v54) // 136 == 0x88
// 0x88 - 0x68 == 0x20
__int64 __fastcall sub_A129A8(__int64 a1, __int64 a2, __int64 a3)
{
for (int i = 0; i <= 15; ++i)
if (*(unsigned __int8 *)(a2 + i)
!= *(unsigned __int8 *)(a3 + i))
return 0;
return 1;
}
JavaScript
복사
The Package::CheckSign function used the 16 bytes at this+0x88 for the MD5 comparison, and the Package::GetName function read the package name as a string starting from this+0x98.
// ota_utils::Package::InitHeader
__int64 __fastcall sub_A125A4(__int64 a1)
{
v1 = sub_A13144("Package_InitHeader#");
v4 = sub_9A098C(v1); // time in nanoseconds
memset((void *)(a1 + 0x68), 0, 0x70u); // initialize header candidate area
*(_DWORD *)(a1 + 0x68) = *(_DWORD *)"UTPK";
*(_DWORD *)(a1 + 0x6C) = 256;
*(_QWORD *)(a1 + 0x70) = v4 / 1000000000; // header 0x08
*(_DWORD *)(a1 + 0x84) = v4 % 1000000000; // header 0x1c
return a1;
}
JavaScript
복사
We also confirmed that the package creation function initializes the header area by 0x70 bytes, so we could estimate that the total header size is 112 bytes.
file size = 341397620
header size = 112
payload size = 341397508
file size - 112 = 341397508
header payload MD5 = a7be7e8921287603adec7c67c66a515d
md5(data[0x70:]) = a7be7e8921287603adec7c67c66a515d
JavaScript
복사
We substituted the offsets identified from the binary into the actual UPK to verify that the values matched. The value obtained by subtracting 112 bytes from the total file size matched exactly the payload size recorded at header 0x10. The 16-byte value at header 0x20 also matched the MD5 computed from the data after 0x70, and finally we confirmed that the TEA\x00 string observed earlier begins at 0x70, where the header ends. Through this process, we determined that the UPK has the following structure.
UPK UTPK header structure
0x0000 UTPK header, 112 bytes
0x0070 payload marker, "TEA\x00"
0x0074 encrypted TAR data
JavaScript
복사
Offset | Size | Field | Purpose confirmed through analysis |
0x00 | 4 | UTPK | Magic value identifying the package format |
0x04 | 4 | format marker | Package format identifier value |
0x08 | 8 | time | Package creation time |
0x10 | 8 | payload size | Size of the payload that follows the header |
0x18 | 4 | flags | Flags that determine whether TAR/TEA processing is applied |
0x1c | 4 | seed | Value used to generate the decryption key |
0x20 | 16 | payload MD5 | Payload integrity comparison value |
0x30 | 64 | package name | Package name |
5.2.1. Composition of the UPK Update Modules
After decrypting the outer UPK and unpacking the tar, we found package.json, module_order.json, and the module directory at the top level. Each module file was again contained as a nested UPK with the same UTPK/TEA structure, so it had to be unpacked recursively.
package_<version>/
├─ package.json
├─ module_order.json
└─ module/
├─ <module_name>/
│ └─ <version>/
│ ├─ module.json
│ └─ file/
│ └─ <module_name>_<version>.upk
└─ ...
Plain Text
복사
package.json records the name and version of each module in the package, the file path, the verification value, and the installation order. Each module’s module.json contains the commands to run before and after installation, the expected exit codes, and the error handling policy. ota_utils and ota_module_utils read this information to back up the existing module, stop the service, swap the files, and run it again.
When we decrypted and decompressed the v1.1.15.1 package we obtained, the actual update targets confirmed were the following three.
1.
network_manager
2.
programming_actuator
3.
webrtc_bridge
Check module list after unpacking the v1.1.15.1 UPK
$ ls extracted/package_1.1.15.1_GO2_Edu_Max_Pro_X_upk/module
network_manager
programming_actuator
webrtc_bridge
Bash
복사
This result shows that OTA is not a scheme that rewrites the entire 59GB eMMC or the 43GB rootfs every time. At least the OTA update package we obtained was presumed to be a structure that only replaces specific robot feature modules under /unitree.
6. Building the Firmware Analysis Environment
6.1. Obtaining SSH on the Latest Firmware Environment
To carry out vulnerability research, the latest firmware version is needed. However, building an analysis environment based on the latest firmware is a separate problem that has to be considered.
On the v1.1.11 firmware currently applied to the Go2, a shell can be obtained with UnLeash-Lite, but once it is updated to a higher version, the v1.1.11 vulnerability can no longer be applied, so the same shell cannot be obtained.
And even if there were a 1-Day vulnerability on a higher version, it is not certain, in an unverified situation, whether a shell could be obtained using that 1-Day vulnerability.
That said, from analyzing the OTA package examined earlier, the update appears to update only the modules in /unitree/module, such as network_manager, programming_actuator, and webrtc_bridge, and the SSH service daemon and Ubuntu’s default init configuration remain as they are in the general system area of p7 linuxroot.
Therefore, if we first configure a persistent SSH setup on the v1.1.11 firmware, where root shell entry is possible, and then perform the OTA update, there is a chance that the SSH configuration and account will be retained even after the /unitree module update.
6.2. Verifying the OTA Update
6.2.1 Updating to the Latest Version
The official OTA update can be performed from the Device → Update menu of the Unitree Go app. In the app, agree to the server connection, start the installation, and keep the robot powered on until the update finishes.
When the update completes, a message indicating the latest version is displayed on the app screen.
Unitree Go APP firmware update
6.2.2 Checking Whether SSH Access Remains
root SSH shell access retained even after the v1.1.15.1 update
ssh root@<robot IP after update>
Bash
복사
After the update, we connected the robot to STA-mode WiFi and attempted SSH access using the newly assigned IP. As expected, the account and SSH port created on v1.1.11 were not blocked and remained alive, so the root shell can be used even on the latest firmware.
root@Unitree:~# grep -R "1.1.15" /unitree/var/log/ota_box/ | tail -1
# ... updatepackage -v 1.1.15 ...
root@Unitree:~# systemctl is-active ssh
active
Bash
복사
From the SSH shell, we can directly confirm, through the robot’s OTA log, that the software has changed to v1.1.15.1.
6.3. Firmware Analysis Environment
Default gdb run on the Go2
pwndbg run in the aarch64 Go2 environment
gdb is installed by default on the Go2, and since the OS installed by default is Ubuntu and connection to the internet is possible, we could freely install the tools needed for analysis.
7. Conclusion
In Part 2 of the series, starting with identifying the UART interface of the Unitree Go2 Pro and obtaining a root shell on the v1.1.11 firmware, we covered extracting the root filesystem and the full eMMC image to grasp the overall firmware structure.
We identified the debugging port by checking the connector presumed to be the mainboard’s UART with a Logic Analyzer, confirmed that the device’s firmware was V1.1.11, obtained a root shell using a 1-Day vulnerability-based tool, and completed the preparation for firmware analysis by extracting the root filesystem and the full and individual eMMC partition images.
And through analyzing the OTA update package structure, we found a way to obtain a root shell on the latest v1.1.15.1 firmware and succeeded in building a dynamic analysis environment.
In the next part (#3), we plan to identify the services running inside the robot by analyzing the system init process, select attack vectors, and then cover the operational structure of each attack vector.
8. References
•
•
[2] A. Bissell, UnLeash-Lite — Open-source Unitree Research Toolkit. https://github.com/a-bissell/UnLeash-Lite
•
•
•
[5] Bin4ry, UniTEABag — Unitree UPK Firmware Decrypt/Encrypt Tool. https://github.com/Bin4ry/UniTEABag

































