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.
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:
- Install
mitmproxyon a laptop - Configure laptop as WiFi hotspot with HTTP proxy
- Install mitmproxy's CA certificate on phone (allows HTTPS decryption)
- Connect phone to laptop hotspot
- 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.