Technical

Reverse Engineering Mobile Apps: Building Better Web Interfaces for Enterprise

When vendors only provide mobile apps with no enterprise controls, we reverse engineer their APIs and build responsive web applications with RBAC, local caching, and unified management—escaping the mobile-only trap.

Automation Services Team

Reverse Engineering Mobile Apps: Building Better Web Interfaces for Enterprise

There's a frustrating trend in industrial equipment and building management: vendors shipping mobile-only control apps with no web interface. For enterprise users, this creates serious problems—you can't manage permissions effectively, user experience differs between iOS and Android, and critical controls are locked behind phone-dependent authentication.

We've been engaged multiple times to reverse engineer these mobile apps, extract the underlying APIs, and build proper web-based management interfaces with role-based access control, better UX, and enterprise features the original apps should have had.

The Mobile-Only Problem

A facilities manager contacted us with a common frustration: their new HVAC system came with a "smart" mobile app for control and monitoring. Sounds great, except:

  • No multi-user support: Each technician needed the shared login credentials
  • No permission granularity: Everyone had full admin rights or nothing
  • Platform inconsistency: iOS version had features the Android version lacked
  • Network dependency: Only worked on WiFi, not the facility's wired management network
  • No integration: Couldn't connect to existing building management system
  • No audit logging: No visibility into who changed what and when

The vendor's response: "Use the app, that's what it's for." No enterprise offering available.

This is where reverse engineering comes in.

The Approach: Traffic Analysis and API Extraction

Mobile apps ultimately communicate with backend servers or local devices via HTTP/HTTPS APIs. If we can intercept and decode that communication, we can replicate it from a web application.

Step 1: HTTPS Interception with mitmproxy

Modern apps use HTTPS, which is encrypted. To see API traffic, we need to perform a man-in-the-middle (MITM) attack on our own network:

Setup:

  1. Install mitmproxy on a laptop
  2. Configure laptop as WiFi hotspot with HTTP proxy
  3. Install mitmproxy's CA certificate on phone (allows HTTPS decryption)
  4. Connect phone to laptop hotspot
  5. Run the mobile app and observe traffic
mitmproxy --mode transparent --showhost

Now every HTTPS request from the app flows through mitmproxy where we can see:

  • Request URLs and methods
  • Headers (including authentication tokens)
  • Request/response bodies
  • Timing and sequencing

Step 2: Documenting the API

We exercised every feature in the mobile app while capturing traffic:

Authentication endpoint:

POST https://api.vendor.com/v2/auth/login
Content-Type: application/json

{
  "username": "user@example.com",
  "password": "password123",
  "device_id": "A1B2C3D4E5F6"
}

Response:
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "8f7d6c5b4a3e2d1c...",
  "expires_in": 3600
}

Device list endpoint:

GET https://api.vendor.com/v2/devices
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Response:
{
  "devices": [
    {
      "id": "dev_12345",
      "name": "Building A HVAC",
      "type": "hvac_controller",
      "status": "online",
      "location": {"floor": 2, "room": "MDF"}
    }
  ]
}

Control endpoint:

POST https://api.vendor.com/v2/devices/dev_12345/control
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Content-Type: application/json

{
  "action": "set_temperature",
  "target_temp": 22.5,
  "mode": "cooling"
}

We documented ~40 endpoints covering authentication, device management, telemetry, configuration, and control operations.

Step 3: Handling Authentication

The API used JWT (JSON Web Tokens) for authentication. The token included:

{
  "sub": "user@example.com",
  "device_id": "A1B2C3D4E5F6",
  "iat": 1698765432,
  "exp": 1698769032
}

The device_id field was interesting—it appeared to be the phone's unique identifier. We tested with randomized device IDs and found the server accepted any value. This meant we could create multiple "virtual devices" for our web interface without needing actual phones.

Step 4: Discovering Local Device Protocol

Some devices could be controlled locally (without cloud API). We used Wireshark to capture local network traffic and found:

  • Devices broadcast UDP discovery packets on port 30120
  • Local control used HTTP (not HTTPS) on port 8080
  • Authentication was a simple API key in the header

This enabled local fallback mode: if internet was down, the web interface could still control devices via LAN.

Building the Web Application: Flask + React

With the API documented, we built a proper web interface.

Backend: Flask API Proxy

The backend acts as a proxy between the frontend and vendor API, adding enterprise features:

from flask import Flask, request, jsonify
import requests
import jwt
from datetime import datetime, timedelta

app = Flask(__name__)

@app.route('/api/auth/login', methods=['POST'])
def login():
    data = request.json
    username = data['username']
    password = data['password']
    
    # Authenticate against vendor API
    vendor_response = requests.post(
        'https://api.vendor.com/v2/auth/login',
        json={
            'username': username,
            'password': password,
            'device_id': generate_device_id()  # Random ID per session
        }
    )
    
    if vendor_response.status_code == 200:
        vendor_token = vendor_response.json()['access_token']
        
        # Create our own JWT with additional claims
        our_token = jwt.encode({
            'sub': username,
            'vendor_token': vendor_token,
            'role': get_user_role(username),  # From our database
            'exp': datetime.utcnow() + timedelta(hours=8)
        }, app.config['SECRET_KEY'])
        
        # Log authentication event
        log_event('login', username, request.remote_addr)
        
        return jsonify({'token': our_token})
    else:
        return jsonify({'error': 'Authentication failed'}), 401

This approach:

  • Validates credentials against vendor API
  • Wraps vendor token in our own JWT with role information
  • Logs authentication events for audit trail
  • Extends session lifetime (vendor tokens expired after 1 hour, ours last 8)

Role-Based Access Control

We implemented proper RBAC:

from functools import wraps

PERMISSIONS = {
    'admin': ['view', 'control', 'configure', 'manage_users'],
    'operator': ['view', 'control'],
    'viewer': ['view']
}

def require_permission(permission):
    def decorator(f):
        @wraps(f)
        def decorated_function(*args, **kwargs):
            token = request.headers.get('Authorization', '').replace('Bearer ', '')
            payload = jwt.decode(token, app.config['SECRET_KEY'], algorithms=['HS256'])
            user_role = payload['role']
            
            if permission not in PERMISSIONS.get(user_role, []):
                return jsonify({'error': 'Insufficient permissions'}), 403
            
            return f(*args, **kwargs)
        return decorated_function
    return decorator

@app.route('/api/devices/<device_id>/control', methods=['POST'])
@require_permission('control')
def control_device(device_id):
    token = jwt.decode(
        request.headers['Authorization'].replace('Bearer ', ''),
        app.config['SECRET_KEY'],
        algorithms=['HS256']
    )
    vendor_token = token['vendor_token']
    
    # Forward request to vendor API with vendor token
    response = requests.post(
        f'https://api.vendor.com/v2/devices/{device_id}/control',
        json=request.json,
        headers={'Authorization': f'Bearer {vendor_token}'}
    )
    
    # Log control action
    log_event('control', token['sub'], f"Device {device_id}: {request.json}")
    
    return jsonify(response.json()), response.status_code

Now administrators can grant "viewer" access to monitoring staff (see status only), "operator" access to technicians (view and control), and "admin" access to facility managers (full control plus user management).

Frontend: React Dashboard

The web interface provides features the mobile app lacked:

Multi-device overview:

function DeviceGrid() {
  const [devices, setDevices] = useState([]);
  
  useEffect(() => {
    fetch('/api/devices', {
      headers: {'Authorization': `Bearer ${getToken()}`}
    })
    .then(r => r.json())
    .then(data => setDevices(data.devices));
  }, []);
  
  return (
    <div className="grid grid-cols-3 gap-4">
      {devices.map(device => (
        <DeviceCard key={device.id} device={device} />
      ))}
    </div>
  );
}

Real-time updates via WebSocket:

useEffect(() => {
  const ws = new WebSocket('wss://our-server.com/ws');
  
  ws.onmessage = (event) => {
    const update = JSON.parse(event.data);
    if (update.type === 'device_status') {
      setDevices(prev => prev.map(d => 
        d.id === update.device_id 
          ? {...d, ...update.data}
          : d
      ));
    }
  };
  
  return () => ws.close();
}, []);

The original mobile app required manual refresh. Our web interface uses WebSockets for live updates pushed from the server.

Batch operations:

function BatchControl() {
  const [selectedDevices, setSelectedDevices] = useState([]);
  
  async function setBatchTemperature(temp) {
    await Promise.all(
      selectedDevices.map(device_id =>
        fetch(`/api/devices/${device_id}/control`, {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${getToken()}`,
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({
            action: 'set_temperature',
            target_temp: temp
          })
        })
      )
    );
  }
  
  return (
    <button onClick={() => setBatchTemperature(22)}>
      Set all to 22°C
    </button>
  );
}

The mobile app controlled one device at a time. The web interface allows selecting multiple devices and controlling them simultaneously—critical for managing large facilities.

Advanced Features: Beyond the Original App

Once we had API access, we could add features the vendor never implemented:

Scheduling and Automation

@app.route('/api/schedules', methods=['POST'])
@require_permission('configure')
def create_schedule():
    schedule = request.json
    # Store in database
    db.schedules.insert({
        'device_id': schedule['device_id'],
        'action': schedule['action'],
        'cron': schedule['cron'],  # e.g., "0 8 * * *" for 8am daily
        'enabled': True
    })
    return jsonify({'status': 'created'})

# Background worker executes schedules
def schedule_worker():
    while True:
        schedules = db.schedules.find({'enabled': True})
        for schedule in schedules:
            if should_run(schedule['cron']):
                execute_device_action(schedule['device_id'], schedule['action'])
        time.sleep(60)

Now users can configure "Set temperature to 18°C at 6pm on weekdays" without manually triggering it daily.

Data Export and Reporting

The mobile app showed current status only. We added historical data tracking:

@app.route('/api/devices/<device_id>/history', methods=['GET'])
@require_permission('view')
def get_device_history(device_id):
    start_date = request.args.get('start', default=datetime.now() - timedelta(days=7))
    end_date = request.args.get('end', default=datetime.now())
    
    data = db.telemetry.find({
        'device_id': device_id,
        'timestamp': {'$gte': start_date, '$lte': end_date}
    })
    
    return jsonify(list(data))

@app.route('/api/reports/export', methods=['POST'])
@require_permission('view')
def export_report():
    report_config = request.json
    devices = report_config['devices']
    start_date = report_config['start_date']
    end_date = report_config['end_date']
    
    # Generate CSV report
    csv_data = generate_csv_report(devices, start_date, end_date)
    
    return Response(
        csv_data,
        mimetype='text/csv',
        headers={'Content-Disposition': 'attachment; filename=report.csv'}
    )

Facility managers can now export usage data for analysis, billing, or compliance reporting.

Integration with Building Management System

We added webhook support so the web interface could trigger external systems:

@app.route('/api/webhooks', methods=['POST'])
@require_permission('admin')
def create_webhook():
    webhook = request.json
    db.webhooks.insert({
        'event_type': webhook['event_type'],  # e.g., 'temperature_threshold'
        'url': webhook['url'],
        'device_id': webhook.get('device_id')
    })
    return jsonify({'status': 'created'})

def trigger_webhooks(event_type, device_id, data):
    webhooks = db.webhooks.find({
        'event_type': event_type,
        '$or': [
            {'device_id': device_id},
            {'device_id': None}  # Global webhooks
        ]
    })
    
    for webhook in webhooks:
        requests.post(webhook['url'], json={
            'event': event_type,
            'device_id': device_id,
            'data': data,
            'timestamp': datetime.now().isoformat()
        })

Now when a temperature threshold is exceeded, the system can notify the existing BMS, trigger alarms, or send alerts—none of which the mobile app supported.

Local Caching: Offline Resilience

Enterprise environments can't afford to lose functionality if internet is unavailable. We implemented local caching:

import redis

cache = redis.Redis(host='localhost', port=6379, db=0)

def get_devices_cached():
    cached = cache.get('devices_list')
    if cached:
        return json.loads(cached)
    
    # Fetch from vendor API
    devices = fetch_devices_from_vendor_api()
    
    # Cache for 5 minutes
    cache.setex('devices_list', 300, json.dumps(devices))
    
    return devices

If vendor API is unreachable, the web interface still shows last-known device status and can control devices via local LAN fallback.

Real-World Deployment Results

We've deployed variations of this approach for several clients across different industries:

HVAC Management System

  • Original limitation: Mobile app for single-building control
  • Our solution: Web interface managing 12 buildings, 200+ zones
  • Impact: 90% reduction in technician dispatch (remote troubleshooting), unified management

Industrial Equipment Monitoring

  • Original limitation: Android-only app with no data export
  • Our solution: Cross-platform web interface with CSV export, API integration
  • Impact: Integrated with maintenance system, automated work order generation

Building Access Control

  • Original limitation: iOS app for managing card readers
  • Our solution: Web dashboard with bulk operations, audit logging
  • Impact: Onboarding time reduced from 2 hours to 15 minutes (batch card provisioning)

Legal and Ethical Considerations

Is reverse engineering legal? In most jurisdictions, yes—with caveats:

  • Interoperability exception: Reverse engineering for interoperability is generally protected
  • No DRM circumvention: If the app has technical protection measures, bypassing them may violate DMCA (US) or similar laws
  • Terms of Service: May violate vendor ToS (though enforceability varies)
  • No redistribution: We're building a private tool, not competing product

Our approach:

  • Client owns the devices and has legitimate need for better control
  • We don't redistribute vendor code or secrets
  • The web interface is for internal use only
  • We don't bypass security—we use the same authentication the app uses

Always consult with legal counsel for your specific situation.

Lessons Learned

API Design Reveals Intent

Good APIs have documentation. Reverse engineering reveals how little thought went into some vendor APIs:

  • Inconsistent naming conventions
  • No versioning strategy
  • Security through obscurity
  • Hard-coded timeouts and limits
  • No rate limiting (we could DOS their servers accidentally)

This told us the vendor never intended third-party integration.

WebSocket > Polling

The mobile app polled for updates every 5 seconds (wasteful). We implemented WebSocket push and saw:

  • 90% reduction in API calls
  • Sub-second latency for status updates
  • Better battery life (less wake-ups)

Certificate Pinning is the Challenge

Some apps implement certificate pinning (only trust specific CA certificates). This prevents MITM interception.

Workarounds:

  • Root the device and disable pinning (requires device control)
  • Patch the APK to remove pinning (Android only, technically complex)
  • Reverse engineer the binary directly (hardest, sometimes necessary)

We've encountered pinned apps twice. Both times we convinced the vendor to provide API documentation rather than spend weeks reversing obfuscated binaries.


Stuck with mobile-only control for critical systems? We reverse engineer proprietary APIs and build enterprise-grade web interfaces with proper access control, audit logging, and integration capabilities. Contact us to discuss your specific situation.