Android Hardware Reverse Engineering

Uncovering Unlisted I2C Devices: Advanced Address Probing on Android

Google AdSense Native Placement - Horizontal Top-Post banner

Introduction: The Hidden World of Android I2C Devices

The Inter-Integrated Circuit (I2C) bus is a cornerstone of modern embedded systems, and Android devices are no exception. From sensors like accelerometers and gyroscopes to power management ICs (PMICs), camera modules, and touch controllers, countless components communicate with the SoC via I2C. While many of these devices are well-documented and appear in system logs or device tree configurations, a subset remains ‘unlisted’ – either dynamically loaded, proprietary, or simply not enumerated in standard system interfaces. This article delves into advanced techniques for uncovering these hidden I2C devices on Android, providing a crucial skill for hardware reverse engineers, security researchers, and custom ROM developers.

Understanding I2C on Android

On Android, like other Linux-based systems, I2C communication is managed by the kernel. User-space applications and drivers interact with I2C buses through device files typically found in /dev/i2c-*. Each file represents a distinct I2C bus controller on the SoC. The kernel’s I2C subsystem handles the low-level clock stretching, start/stop conditions, and acknowledgment bits, exposing a simpler interface for higher-level drivers.

Identifying I2C Bus Controllers

The first step in any I2C investigation is to identify the available buses. You can do this by listing the device files:

adb shell ls -l /dev/i2c-*

This command typically reveals a list like /dev/i2c-0, /dev/i2c-1, and so on. The number of buses varies significantly between devices, depending on the SoC and its peripheral integration. To gain context about what each bus might be connected to, examining kernel boot logs is invaluable:

adb shell dmesg | grep -i "i2c"

Look for messages indicating I2C adapter registration, driver probes, and device attachments. These logs can often hint at which buses are active and what devices are expected to be on them.

The Challenge of Unlisted Devices

Why would an I2C device be ‘unlisted’? Common reasons include:

  • Proprietary Drivers: Manufacturers may use custom drivers that don’t fully expose device information through standard sysfs interfaces.
  • Dynamic Enumeration: Devices might be probed and configured only when specific conditions are met (e.g., camera power-up).
  • Kernel Configuration: The I2C client device might not be statically defined in the kernel’s device tree or board files.
  • Security Obscurity: In some cases, vendors might intentionally make it harder to identify certain sensitive components.

Advanced Probing Techniques

1. Leveraging i2cdetect (If Available)

The standard Linux tool for I2C device probing is i2cdetect. It scans a specified I2C bus for devices at all possible 7-bit addresses and reports any acknowledgments. However, i2cdetect is rarely pre-installed on production Android devices due to its diagnostic nature. If you have a rooted device or can compile custom binaries, it’s a powerful tool.

Compiling i2cdetect for Android:

You’ll need the Android NDK and a cross-compilation environment. Here’s a simplified approach:

# Download i2c-tools source (e.g., from kernel.org)tar -xf i2c-tools-*.tar.gzcd i2c-tools-*# Set up NDK environmentexport NDK_ROOT=/path/to/android-ndk-rXXexport PATH=$PATH:$NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/binexport TARGET_HOST=aarch64-linux-androidXX  # Adjust based on Android API level and arch# Compile (adjust makefile or use direct GCC/Clang commands)make CC=${TARGET_HOST}-clang BUILD_STATIC=y -C tools/i2cgetmake CC=${TARGET_HOST}-clang BUILD_STATIC=y -C tools/i2csetmake CC=${TARGET_HOST}-clang BUILD_STATIC=y -C tools/i2cdetect# Push to deviceadb push tools/i2cdetect/i2cdetect /data/local/tmp/adb shell chmod +x /data/local/tmp/i2cdetect

Once on the device, you can run:

adb shell /data/local/tmp/i2cdetect -y [BUS_NUMBER]

For example, to scan bus 1:

adb shell /data/local/tmp/i2cdetect -y 1

An output showing hex values (not --) indicates a device acknowledged at that address.

2. Manual Probing with i2c-dev (C Code)

When i2cdetect is not an option or for more granular control, writing a simple C program to manually probe addresses is the most robust method. This involves opening the I2C device file, setting the slave address, and attempting a dummy read or write.

Probing C Code Example:

#include <stdio.h>#include <stdlib.h>#include <string.h>#include <fcntl.h>#include <unistd.h>#include <errno.h>#include <sys/ioctl.h>#include <linux/i2c-dev.h> // Required for I2C_SLAVE#define I2C_BUS_PATH_FORMAT "/dev/i2c-%d"int main(int argc, char *argv[]) {    int fd;    char filename[20];    int i2c_bus_num;    if (argc < 2) {        fprintf(stderr, "Usage: %s <i2c_bus_number>n", argv[0]);        return 1;    }    i2c_bus_num = atoi(argv[1]);    if (i2c_bus_num < 0) {        fprintf(stderr, "Invalid I2C bus number.n");        return 1;    }    snprintf(filename, sizeof(filename), I2C_BUS_PATH_FORMAT, i2c_bus_num);    fd = open(filename, O_RDWR);    if (fd < 0) {        fprintf(stderr, "Failed to open I2C bus %s: %sn", filename, strerror(errno));        return 1;    }    printf("Scanning I2C bus %d (0x03-0x77):n", i2c_bus_num);    printf("   0  1  2  3  4  5  6  7  8  9  a  b  c  d  e  fn");    for (int addr = 0x00; addr <= 0x77; addr++) {        if (addr % 16 == 0) {            printf("%02x:", addr);        }        // Skip reserved addresses and general call address        if (addr <= 0x02 || addr >= 0x78) {            printf("   ");            continue;        }        if (ioctl(fd, I2C_SLAVE, addr) < 0) {            // This is expected for non-existent devices,            // but a successful ioctl doesn't mean device exists either.            // We need to attempt a read/write.            // fprintf(stderr, "Warning: ioctl failed for address 0x%02x: %sn", addr, strerror(errno));            printf(" --"); // ioctl failure is usually not a device present            if (addr % 16 == 15) printf("n");            continue;        }        // Attempt a dummy read. A successful read indicates presence.        // Write can also be used, but read is generally safer.        char buffer[1];        if (read(fd, buffer, 0) == 0) { // Read 0 bytes to just check ACK            printf(" %02x", addr);        } else {            // If read fails for any reason (e.g., NACK, I/O error), no device here.            printf(" --");        }        if (addr % 16 == 15) printf("n");    }    close(fd);    return 0;}

To compile and run this on Android:

# Compile (using your NDK setup from above)${TARGET_HOST}-clang -o i2c_scanner i2c_scanner.c -static# Push to deviceadb push i2c_scanner /data/local/tmp/adb shell chmod +x /data/local/tmp/i2c_scanner# Run (e.g., scan bus 1)adb shell /data/local/tmp/i2c_scanner 1

This program iterates through all possible 7-bit I2C addresses (0x03 to 0x77), attempts to set the slave address using I2C_SLAVE ioctl, and then performs a zero-byte read. If the read returns 0 (indicating a successful transaction with no data transfer, just an ACK from the slave), it means a device is present at that address. The output format mimics i2cdetect.

3. Analyzing Kernel Device Tree (DTB)

The Device Tree Blob (DTB) is a critical component on modern Linux systems, including Android. It describes the hardware components connected to the SoC, including I2C devices and their addresses. Even if a device doesn’t show up via dynamic probing, it might be statically defined in the DTB.

Extracting and Decompiling the DTB:

  1. Obtain boot.img: This usually requires unlocking the bootloader or finding stock firmware.
  2. Extract DTB: Use tools like Adb-Fastboot-Tool or magiskboot to extract the DTB blob from the boot.img. The DTB is often located in the boot image or a separate dtbo.img partition.
  3. Decompile DTB: Use the Device Tree Compiler (dtc) to convert the binary DTB into a human-readable Device Tree Source (DTS) file.
dtc -I dtb -O dts -o device_tree.dts [path/to/extracted/dtb_blob]

Once you have the .dts file, open it and search for I2C-related nodes. Look for nodes similar to this:

i2c@78b5000 {    compatible = "qcom,i2c-qcom-geni";    reg = <0x0 0x78b5000 0x0 0x1000>;    clocks = <&clock_gcc RPMH_BCR_QUPV3_I2C_APPS_CLK>;    #address-cells = <1>;    #size-cells = <0>;    status = "ok";    my_hidden_sensor@1a {        compatible = "vendor,my-hidden-sensor";        reg = <0x1a>;        interrupt-parent = <&tlmm>;        interrupts = <95 0x2>;        status = "okay";    };};

In this example, my_hidden_sensor@1a explicitly defines an I2C device with address 0x1a. The compatible string is crucial as it indicates the kernel driver expected to bind to this device. Even if this device isn’t probed by your manual scanner (perhaps due to being dynamically turned on), its presence in the DTB confirms its existence and expected address.

Conclusion

Uncovering unlisted I2C devices on Android is a vital skill for anyone delving into low-level hardware analysis. By combining systematic bus identification, intelligent use of tools like i2cdetect, robust manual probing with custom C code, and meticulous analysis of the kernel device tree, you can bring these hidden components to light. This advanced methodology not only aids in reverse engineering proprietary hardware but also strengthens security assessments by revealing undocumented attack surfaces and helps in building truly custom Android experiences. Remember to always proceed with caution and respect for device integrity when performing such deep-level analysis.

Android Mobile Specs & Compare Directory

Are you researching mobile hardware properties, processor SoCs, GPU chipsets, or RAM configurations? Access our complete specs catalog to compare up to 5 devices side-by-side!

Compare Devices Specs →
Google AdSense Inline Placement - Content Footer banner