Reverse-engineering an OBi302 ATA

2026-03-14

Lately, I have been tinkering with IP phones and PBXes like Asterisk. I have found Polycom (now HP and formerly Plantronics/Poly) VVX phones to work well so when I saw a used Polycom analog telephone adapter (ATA) for cheap, I picked it up in order to potentially interface with analog phones as well. The unit I picked up has 2 analog telephone ports as well as two Ethernet ports. This OBi302 was originally designed by a company called Obihai Technology which made ATAs to interface with Google Voice. However, according to online documentation, they can be configured to interface with standard PBXex via SIP, etc.

I powered the device up, LEDs started blinking, however, I was unable to log into the web interface on either the WAN port or the LAN port which features a NAT for some reason: the default credentials I had found online did not work. The situation didn't change after performing a factory reset. I did capture the network traffic on the WAN network interface and saw DNS queries for domains like root.pnn.obihai.com, ntp.telio.no and initial.obihai.prov.telio.no. While the former didn't seem particularly suspicious, the latter two indicate that this device had been customized for the Norwegian IP telco Telio. It seemed plausible that they provisioned the ATA with custom credentials as well. I had read in the ATA's documentation that it can also be configured via the phone ports, however, I could not get this to work either (foreshadowing).

UART console

Opening up the device revealed no huge suprises: the main SoC Marvell 88E7200-LKJ2 combines ARM9 CPU and Ethernet switch and is connected to 64 MiB of DDR2 SDRAM and 16 MiB of serial NOR flash memory. The analog phone ports are implemented using the Skyworks Si32260 IC. The remainder is basically power regulation. There is also an unpopulated radio, perhaps DECT? The first step was now to find a serial console interface. The unpopulated J17 pinheader is a likely candidate which proved to be correct; pinout: 1-TX 2-RX 3-3V3 4-GND.

OBi302 PCB
internals

Unfortunately, the UART console didn't get me anywhere. All I could see was

Uncompressing Linux...<SNIP>... done, booting the kernel.

OBi202 login:

Actually, later on, I could I did not get the login prompt anymore so it must be shown conditionally. I could not break into the bootloader. Neither did simple root passwords work. This left me no option but desolder the flash chip and read it out in the hopes of finding the credentials. They must be stored somwhere on the device. However, it's not certain that I would be able to read them out if they happen to be encrypted by a hardware cryptographic key.

Reading out the flash ROM

I used flashrom together with a generic FT232H breakout board connected via jumper wires to a ZIF socket for the flash chip after having desoldered it. I struggled a lot to read out the device until I realized that I needed to pull /HOLD and /WP highwhich resolved all initial issues. This is how the W25Q128JV flash chip must be connected to the FT232H:

FT232HW25Q128JV
DBUS0CLK
DBUS1DI
DBUS2DO
DBUS3/CS
/HOLD → VCC
/WP → VCC
flashrom interface

flashrom then gets invoked like so:

flashrom -p ft2232_spi:type=232H --{read,write} flash.bin

The first thing I typically do when faced with an unknown flash blob is running binwalk on it to hopefully discover partitions, etc. These serial NOR flashes don't typically contain the partition table. Instead, the bootloader typically supplies the partitions via device tree, kernel parameters, etc. binwalk finds the following:

Offset Description
0x50000 uImage firmware image, header size: 64 bytes, data size: 2505264 bytes, compression: none, CPU: ARM, OS: Linux, image type: OS Kernel Image, load address: 0x8000, entry point: 0x8000, creation time: 2017-01-24 22:52:32, image name: "Linux-2.6.30.10"
0x2D0000 uImage firmware image, header size: 64 bytes, data size: 1229540 bytes, compression: none, CPU: ARM, OS: Linux, image type: OS Kernel Image, load address: 0x8000, entry point: 0x8000, creation time: 2011-10-12 21:50:39, image name: "Linux-2.6.30.10"
0x480000 SquashFS file system, little endian, version: 4.0, compression: gzip, inode count: 826, block size: 131072, image size: 5137593 bytes, created: 2018-06-14 19:29:57
0xB40000 JFFS2 filesystem, little endian, nodes: 6, total size: 327692 bytes
0xBA4410 JFFS2 filesystem, little endian, nodes: 8, total size: 310268 bytes
0xC00000 SquashFS file system, little endian, version: 4.0, compression: gzip, inode count: 153, block size: 131072, image size: 1989074 bytes, created: 2018-06-30 01:59:34
0xE40000 SquashFS file system, little endian, version: 4.0, compression: gzip, inode count: 437, block size: 131072, image size: 742658 bytes, created: 2017-01-09 19:31:48
0xF00000 SquashFS file system, little endian, version: 4.0, compression: gzip, inode count: 13, block size: 131072, image size: 982209 bytes, created: 2016-05-04 22:18:43
binwalk results

That's a lot to unpack (ba dum tiss) but we can start to categorize a little: We have 2 ARM Linux kernel images, a few SquashFS file systems that are read-only and a couple of JFFS2 file systems that are likely used for storing persistent data (settings perhaps?).

Looking at the output of strings before 0x50000 reveals signs of a U-Boot bootloader and its environment:

bootargs=root=/dev/mtdblock5 rootfstype=squashfs mem=56M brd=$(boardID)
bootcmd=bootm 800000
bootdelay=0
baudrate=115200
ethaddr=08:00:3e:26:0a:5b
ipaddr=192.168.15.33
serverip=192.168.15.102
netmask=255.255.255.000
bootfile=bootrom.bin
boardID=DB
silent=1

What if we could set bootdelay to a positive integer and set silent to 0? I tried the naive approach and modify the binary and write it back to the flash chip. Unsurprisingly, this does not work: the device shows no sign of life. There must be a checksum or even a cryptographic signature somewhere.

After finding out that this family of SoCs ("Link Street") uses the Kirkwood Boot Image format (thanks, Gemini) I could use the U-Boot mkimage source code to map the fields in the image header. Here are the highlights:

OffsetFieldValue
0x00Boot Type0x00 (DEF_ATTRIB)
0x04Block Size0x00038e04
0x0cSource Address0x00000140
0x10Dest. Address0x00100000
0x14Exec. Address0x00100000
kwbimage header

So the U-Boot code starts at offset 0x140, is 0x38e04 long and gets loaded to 0x00100000 for execution. This allowed me to extract this blob and load it into Ghidra with base address 0x00100000. Curiously, this first thing the U-Boot executable does is copy itself to 0x00380000 and jump to it. So in order to get a sensible decompilation, the U-Boot blob needs to be loaded with base address 0x00380000. This is, however, where I abandoned this strategy. Instead, I wanted to focus on the other partitions and actually find the factory defaults.

Finding the partition layout

Since I hadn't seen any traces of a device tree blob or kernel parameters that resembled a partition layout in the U-Boot blob, I suspected that the partition table might be hard-coded into the kernel. So I extracted the first kernel image, stripped the U-Boot header and extracted the self-extracting kernel image using a slightly modified version of extract-vmlinux. Then, I could run strings against the decompressed kernel and grep for common partition labels like rootfs and kernel. This yielded the following promising sequence of strings: spi_flash0, u-boot, scratch, rootfs, obi app, bluetooth. Looking where these strings are referenced (offset by 0xc0008000) leads us to the partition table (struct flash_platform_data and an array of struct mtd_partition) which boils down to this partition table:

LabelOffsetSizeDevice
u-boot0x000000000x00050000mtd2
kernel0x000500000x00280000mtd3
scratch0x00b400000x000c0000mtd4
rootfs0x004800000x006c0000mtd5
flash00x000000000x01000000mtd6
obi app0x00c000000x00240000mtd7
bluetooth0x00f000000x00100000mtd8
flash partitions

Inspecting the partitions one by one did not yield the data I was looking for. Neither did I find unit-specific data like certificates and keys. However, I did find the mount points from the init script /etc/rc on the rootfs partition:

DeviceMount point
/dev/mtdblock4/scratch
/dev/mtdblock7/obi
/dev/mtdblock8/bluetooth
flash partition mount points

The last thing the init script does is launching /obi/obi in the background. The obi script launches obid in the background which turns out to be a daemon that allows the main app to restart itself, etc. Finally, the obi script launches /obi/obiapp.

Decompiling obiapp

The executable obiapp on partition mtd7 appears to run the show on the device. I spent way too much time wading through the decompiled code in Ghidra, annotating along the way. What eventually led me down the most fruitful road was searching for strings containing mtd. This led me to 3 functions responsible for reading, erasing and writing /dev/mtdchar6 which is just an abstraction for the entire flash chip. Traversing the call tree backwards let me find various offsets in the flash chip used for various data:

OffsetSizePurpose
0x0400000x0ff00unit data
0x04ff000x00100u-boot build date
0x4000000x60000config data
0x4600000x10000factory default config data
0x4700000x00400last firmware update pack header
non-partition data in flash

Decrypting unit & config data

Parts of the unit data and the config data are RC4-encrypted. However, investigating the decompilation reveals that the key is derived from known data. The unit data key is comprised of the first 15 MD5 hash bytes of the unit data header concatenated with the string thisisthesecretofobihaimfd 🙄 I believe doing this kind of stuff makes it actually more likely for people to reverse-engineer key derivation scheme since a string like that immediately catches the eye when running strings on the executable. The unit data contains the following data:

The config data key is composed of the config block's header combined with the unit's masked MAC address. The factory defaults block reveals the following strings among other things:

So there we have it: the Telio domains and the admin password is adminpassword. And it works. I now feel dumb not trying that one before reverse-engineering this whole thing. Oh well...

When I was finally able to configure the ATA, I found that the analog phone front-end chip was likely broken on my unit. At least, I couldn't get it to work and the series resistors on the phone lines were burned out -- bummer! At least the journey was fun and educational.

Close-up of destroyed resistors in the front-end of the analog telephone ports
compare R156/R155 to R153/R154

Appendix

The scripts to extract and decrypt unit and config data are available here: