Overview
The Android Debug Bridge (ADB) is the core technology enabling this tool to communicate with Android devices. Understanding ADB’s architecture and the tool’s implementation helps troubleshoot issues and extend functionality.
ADB Architecture
Components
ADB operates as a client-server architecture with three components:
ADB Client : Command-line tool that sends commands
ADB Server : Background daemon that manages device connections
ADB Daemon (adbd) : Runs on the Android device, executes commands
┌─────────────────┐
│ Tool (Client) │
└────────┬────────┘
│ TCP/IP (localhost:5037)
▼
┌─────────────────┐
│ ADB Server │ ← Manages device connections
└────────┬────────┘
│ USB or TCP/IP
▼
┌─────────────────┐
│ adbd (Device) │ ← Runs commands on device
└─────────────────┘
Communication Protocol
Client → Server : TCP port 5037 (localhost)
Server → Device : USB or TCP/IP (wireless ADB)
Protocol : Custom binary protocol with length-prefixed messages
DeviceManager Class
Location : core/device_manager.py:13-283
The DeviceManager class consolidates all ADB operations:
class DeviceManager :
ADB_URLS = { # Platform-specific download URLs
'windows' : 'https://dl.google.com/android/.../platform-tools-latest-windows.zip' ,
'linux' : '...platform-tools-latest-linux.zip' ,
'darwin' : '...platform-tools-latest-darwin.zip'
}
def __init__ ( self , adb_dir : str = "adb" , output_dir : str = "backups" ):
self .adb_dir = os.path.abspath(adb_dir)
self .adb_executable = self ._get_adb_path()
ADB Executable Detection
Method : _get_adb_path() at line 37-48
def _get_adb_path ( self ) -> str :
exe_name = "adb.exe" if self .os_type == 'windows' else "adb"
# Check platform-tools subdirectory (standard location)
potential_path = os.path.join( self .adb_dir, "platform-tools" , exe_name)
if os.path.exists(potential_path):
return potential_path
# Check adb directory directly (fallback)
potential_path = os.path.join( self .adb_dir, exe_name)
if os.path.exists(potential_path):
return potential_path
return "" # Not found
Paths checked (in order):
adb/platform-tools/adb[.exe] (standard)
adb/adb[.exe] (fallback)
Automatic ADB Download
Method : download_adb() at line 53-66
def download_adb ( self ):
url = self . ADB_URLS .get( self .os_type)
print_info( f "Downloading ADB for { self .os_type } ..." )
response = requests.get(url, stream = True )
with zipfile.ZipFile(io.BytesIO(response.content)) as z:
z.extractall( self .adb_dir) # Extracts to adb/platform-tools/
self .adb_executable = self ._get_adb_path()
# Make executable on Unix systems
if self .os_type != 'windows' and self .adb_executable:
os.chmod( self .adb_executable, 0o 755 )
Called from : main.py:86-93 during startup if ADB not found
The tool downloads official Google SDK platform-tools directly from dl.google.com. This ensures latest ADB version with security updates.
Command Execution
Core Method: run_command()
Location : core/device_manager.py:68-80
All ADB operations use this method:
def run_command ( self , args : List[ str ]) -> Tuple[ int , str , str ]:
if not self .is_installed():
return - 1 , "" , "ADB not installed."
cmd = [ self .adb_executable] + args
startupinfo = None
if self .os_type == 'windows' :
# Hide console window on Windows
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess. STARTF_USESHOWWINDOW
process = subprocess.Popen(
cmd,
stdout = subprocess. PIPE ,
stderr = subprocess. PIPE ,
text = True ,
startupinfo = startupinfo
)
stdout, stderr = process.communicate()
return process.returncode, stdout, stderr
Returns : (exit_code, stdout, stderr)
exit_code : 0 = success, non-zero = error
stdout : Command output
stderr : Error messages
Example Usage
from core.device_manager import DeviceManager
dm = DeviceManager()
# List devices
code, stdout, stderr = dm.run_command([ 'devices' ])
print (stdout)
# Output:
# List of devices attached
# ABC123456789 device
# Get device property
code, stdout, stderr = dm.run_command([ '-s' , 'ABC123456789' , 'shell' , 'getprop' , 'ro.product.model' ])
print (stdout.strip()) # Device model name
Device Discovery
Listing Connected Devices
Method : get_devices() at line 83-90
def get_devices ( self ) -> List[ str ]:
code, stdout, stderr = self .run_command([ 'devices' ])
if code != 0 :
return []
devices = []
for line in stdout.strip().split( ' \n ' )[ 1 :]:
if ' \t device' in line:
devices.append(line.split( ' \t ' )[ 0 ])
return devices
Example output parsing :
Raw adb devices output:
List of devices attached
ABC123456789 device
DEF987654321 unauthorized
GHI111222333 offline
Parsed result:
[ 'ABC123456789' ] # Only returns devices with 'device' status
Devices showing “unauthorized” or “offline” are excluded. User must accept USB debugging prompt for “unauthorized” devices.
Method : get_detailed_device_info() at line 92-143
Queries multiple system properties and services:
def get_detailed_device_info ( self , serial : str ) -> dict :
props = {
'Model' : 'ro.product.model' ,
'Brand' : 'ro.product.brand' ,
'Android Version' : 'ro.build.version.release' ,
'SDK' : 'ro.build.version.sdk' ,
'Manufacturer' : 'ro.product.manufacturer' ,
# ...
}
info = { 'Serial' : serial}
for key, prop in props.items():
_, val, _ = self .run_command([ '-s' , serial, 'shell' , 'getprop' , prop])
info[key] = val.strip() if val else "Unknown"
# Battery info from dumpsys
_, bat, _ = self .run_command([ '-s' , serial, 'shell' , 'dumpsys' , 'battery' ])
if bat:
level = re.search( r 'level: ( \d + ) ' , bat)
info[ 'Battery' ] = f " { level.group( 1 ) } %" if level else "Unknown"
# Memory info from /proc/meminfo
_, mem, _ = self .run_command([ '-s' , serial, 'shell' , 'cat' , '/proc/meminfo' ])
# Parse MemTotal, MemAvailable...
return info
Properties queried :
ro.product.model - Device model (e.g., “Pixel 5”)
ro.product.brand - Brand (e.g., “Google”)
ro.build.version.release - Android version (e.g., “13”)
ro.build.version.sdk - SDK API level (e.g., “33”)
dumpsys battery - Battery level
/proc/meminfo - RAM statistics
df -h /data - Storage information
Multi-Device Handling
Serial Number Targeting
When multiple devices are connected, the -s flag specifies target device:
# Wrong: Fails with "more than one device"
self .run_command([ 'shell' , 'ls' , '/sdcard' ])
# Correct: Targets specific device
self .run_command([ '-s' , device_serial, 'shell' , 'ls' , '/sdcard' ])
Tool implementation : All device-specific commands include -s flag (core/device_manager.py:101, 138, 158, etc.)
User Selection
When multiple devices detected, tool prompts for selection (main.py:167-175):
if len (devices) == 1 :
self .selected_device = devices[ 0 ] # Auto-select
else :
sel = ui.ask( "Select device number (or Enter to skip)" , default = "" )
if sel.isdigit() and 1 <= int (sel) <= len (devices):
idx = int (sel) - 1
self .selected_device = devices[idx]
Shell Command Execution
Basic Shell Commands
# Run command in device shell
code, stdout, stderr = dm.run_command([ '-s' , serial, 'shell' , 'ls' , '-l' , '/sdcard' ])
# Multiple arguments with quotes (for paths with spaces)
code, stdout, stderr = dm.run_command([ '-s' , serial, 'shell' , 'ls' , '"/sdcard/WhatsApp Business"' ])
Quotes are preserved in Python list—no need for shell escaping. Use '"path"' for paths with spaces.
Package Manager Queries
Check installed packages (core/device_manager.py:155-161):
def check_packages ( self , device_id : str ) -> List[ str ]:
installed = []
for pkg in [ 'com.whatsapp' , 'com.whatsapp.w4b' ]:
code, stdout, _ = self .run_command([ '-s' , device_id, 'shell' , 'pm' , 'path' , pkg])
if code == 0 and stdout.strip():
installed.append(pkg)
return installed
Command : pm path <package> returns package APK path if installed:
# Installed:
$ adb shell pm path com.whatsapp
package:/data/app/com.whatsapp-xxx/base.apk
# Not installed:
$ adb shell pm path com.nonexistent
# (no output, exit code 1)
User Enumeration
Multi-user support (core/device_manager.py:145-153):
def get_users ( self , device_id : str ) -> List[Dict[ str , str ]]:
code, stdout, _ = self .run_command([ '-s' , device_id, 'shell' , 'pm' , 'list' , 'users' ])
users = []
for line in stdout.split( ' \n ' ):
match = re.search( r 'UserInfo \{ ( \d + ) : ([ ^ : ] + ) :' , line)
if match:
users.append({ 'id' : match.group( 1 ), 'name' : match.group( 2 )})
return users
Example output :
Users:
UserInfo{0:Owner:c13} running
UserInfo{10:Work Profile:1030} running
Parsed:
[
{ 'id' : '0' , 'name' : 'Owner' },
{ 'id' : '10' , 'name' : 'Work Profile' }
]
File Operations
File Pull (Device → Computer)
Method : dump_backup() at line 238-252
def dump_backup ( self , device_id : str , remote_path : str ,
user_id : str , package_type : str ) -> str :
filename = os.path.basename(remote_path)
local_dir = os.path.join( self .output_dir, device_id, f "user_ { user_id } " , package_type)
os.makedirs(local_dir, exist_ok = True )
local_path = os.path.join(local_dir, filename)
# ADB pull command
code, _, stderr = self .run_command([ '-s' , device_id, 'pull' , remote_path, local_path])
if code == 0 and os.path.exists(local_path):
return local_path
else :
print_error( f "Failed to pull { remote_path } : { stderr } " )
return ""
ADB pull syntax :
adb pull < remote_pat h > < local_pat h >
Example :
adb -s ABC123 pull "/sdcard/WhatsApp/Databases/msgstore.db.crypt14" "./backups/msgstore.db.crypt14"
File Push (Computer → Device)
Used by “Deploy to Termux” feature (main.py:347-360):
self .device_manager.run_command([ '-s' , serial, 'push' , 'main.py' , f ' { target_base } /' ])
ADB push syntax :
adb push < local_pat h > < remote_pat h >
Bulk File Transfer with Progress
Method : dump_media_with_progress() at line 254-282
For large media folders, uses subprocess with stdout streaming:
def dump_media_with_progress ( self , device_id : str , remote_path : str , local_dir : str ):
# Count files first
full_cmd = f ' { self .adb_executable } -s { device_id } shell "find \' { remote_path } \' -type f | wc -l"'
process = subprocess.run(full_cmd, shell = True , capture_output = True , text = True )
total_files = int (process.stdout.strip())
# Pull with progress bar
cmd_pull = [ self .adb_executable, '-s' , device_id, 'pull' , f " { remote_path } /." , local_dir]
process = subprocess.Popen(cmd_pull, stdout = subprocess. PIPE , stderr = subprocess. STDOUT ,
universal_newlines = True )
pbar = tqdm( total = total_files, unit = "file" )
for line in process.stdout:
if line.startswith( "[" ) or "->" in line: # Progress indicators
pbar.update( 1 )
pbar.close()
Why shell=True for file count : The find | wc -l pipeline requires shell interpretation. For security, only used with controlled paths.
Security Note : Using shell=True can be dangerous with user input. The tool only uses it with internal, validated paths.
Storage Paths
Accessible Storage (No Root Required)
Primary external storage : /sdcard/ or /storage/emulated/0/
base = f "/storage/emulated/ { user_id } /"
WhatsApp paths (core/device_manager.py:165-170):
# Legacy path (Android <11)
paths = [base + "WhatsApp/Databases" ]
# Scoped storage path (Android 11+)
paths.append(base + "Android/media/com.whatsapp/WhatsApp/Databases" )
Why two paths?
Android 11+ introduced scoped storage , restricting app access to specific directories. WhatsApp moved backups to Android/media/<package>/ to comply.
Protected Storage (Root Required)
App internal storage : /data/data/<package>/
Contains:
files/key - Encryption key (crypt14/15)
databases/msgstore.db - Live database (unencrypted)
shared_prefs/ - App preferences
# Requires root
adb shell
su
cat /data/data/com.whatsapp/files/key | xxd -p
Accessing /data/data/ requires:
Rooted device (with SuperSU, Magisk, etc.)
USB debugging + Root debugging enabled
Or using Termux (which has app-level access as WhatsApp’s user)
Multi-User Paths
Devices with multiple users have separate storage:
User 0 (primary): /storage/emulated/0/
User 10 (work): /storage/emulated/10/
Tool queries all users and scans each (main.py:238-250)
Root vs Non-Root Access
Non-Root (Standard)
Accessible :
External storage (/sdcard/, /storage/emulated/)
Backup files (.crypt14 databases)
Media files
Not accessible :
Encryption keys (/data/data/*/files/key)
Live databases (/data/data/*/databases/)
Protected app data
Root Access
Enables :
Direct key extraction
Live database access (no backup needed)
System modifications
How to use :
adb shell
su # Request root (requires rooted device)
# Extract key
cat /data/data/com.whatsapp/files/key | xxd -p > /sdcard/wa_key.txt
exit
exit
adb pull /sdcard/wa_key.txt
Termux Alternative
Termux provides app-level access without root:
Install Termux from F-Droid (NOT Google Play—outdated)
Deploy tool to Termux: Menu → “Deploy to Termux”
Run inside Termux app:
cd ~/whatsapp-forensic-tool
python main.py
Termux can access WhatsApp’s shared storage directly.
ADB Backup Command (Alternative Method)
Overview
ADB provides a built-in backup mechanism:
adb backup -f backup.ab -noapk com.whatsapp
Creates a compressed, optionally encrypted .ab (Android Backup) file.
Limitations
Why the tool doesn’t use adb backup :
Deprecated : Removed in Android 12+ (API 31)
Requires user interaction : User must unlock device and confirm backup
Optional encryption : User can set passphrase, making extraction complex
Slow : Compresses entire app data, taking minutes
Unreliable : Many manufacturers disable it
Direct file pull (adb pull) is faster, more reliable, and works on modern Android.
For legacy .ab backups:
# Unpack .ab to .tar
( dd if=backup.ab bs= 24 skip= 1 | openssl zlib -d ) > backup.tar
# Extract .tar
tar -xvf backup.tar
# Find database
find . -name "msgstore.db.crypt*"
Error Handling
Common Return Codes
code, stdout, stderr = dm.run_command([ ... ])
if code != 0 :
if "device not found" in stderr:
# Device disconnected
elif "device unauthorized" in stderr:
# USB debugging not authorized
elif "no such file" in stderr.lower():
# File/directory doesn't exist
else :
# Generic error
Timeout Handling
Long operations (large file transfers) may need timeouts:
import signal
def timeout_handler ( signum , frame ):
raise TimeoutError ( "ADB command timed out" )
# Set 60-second timeout (Unix only)
signal.signal(signal. SIGALRM , timeout_handler)
signal.alarm( 60 )
try :
code, stdout, stderr = dm.run_command([ ... ])
except TimeoutError :
print ( "Command took too long" )
finally :
signal.alarm( 0 ) # Cancel alarm
The tool’s run_command() doesn’t implement timeouts by default. For production use, consider adding timeout parameter using subprocess.run(timeout=...).
Advanced Operations
Wireless ADB (TCP/IP Mode)
Connect to device over WiFi:
# 1. Connect via USB first
adb devices
# 2. Enable TCP/IP mode on port 5555
adb tcpip 5555
# 3. Find device IP (in device WiFi settings or)
adb shell ip addr show wlan0
# 4. Connect wirelessly
adb connect 192.168.1.100:5555
# 5. Disconnect USB, verify
adb devices
Tool supports wireless devices automatically (serial will be 192.168.1.100:5555)
Port Forwarding
Forward device port to computer:
# Forward device:8080 to localhost:8080
adb forward tcp:8080 tcp:8080
# Use in tool
code, stdout, stderr = dm.run_command ([ 'forward' , 'tcp:8080' , 'tcp:8080' ])
Useful for accessing device web servers or services.
Logcat Monitoring
# Read live logs
code, stdout, stderr = dm.run_command([ '-s' , serial, 'logcat' , '-d' , '-s' , 'WhatsApp:V' ])
print (stdout) # WhatsApp-specific logs
Filters :
-d: Dump and exit (don’t follow)
-s <tag>:V: Show verbose logs for specific tag
-c: Clear log buffer
USB 2.0 vs 3.0
USB 2.0 : ~40 MB/s transfer speed (sufficient for most backups)
USB 3.0 : ~100+ MB/s (better for large media folders)
Tool works with both. For 10GB+ media dumps, use USB 3.0 ports.
Parallel Operations
ADB server can handle multiple simultaneous commands:
import threading
def pull_file ( remote , local ):
dm.run_command([ '-s' , serial, 'pull' , remote, local])
# Pull multiple files concurrently
threads = [
threading.Thread( target = pull_file, args = ( f '/sdcard/file { i } .db' , f 'file { i } .db' ))
for i in range ( 5 )
]
for t in threads: t.start()
for t in threads: t.join()
Parallel pulls can saturate USB bandwidth. For 5+ concurrent operations, sequential may be faster.
Security Considerations
USB Debugging Risks
USB debugging grants:
File access : Read/write to accessible storage
App installation : Install arbitrary APKs
Shell access : Execute commands as shell user
Backup/restore : Extract app data
Best practices :
Disable USB debugging when not in use
Only authorize trusted computers
Revoke authorization regularly: Developer Options → “Revoke USB debugging authorizations”
Authorization Mechanism
First USB debugging connection requires on-device confirmation. Computer’s RSA public key is stored in /data/misc/adb/adb_keys on device.
Revoke all :
adb shell rm /data/misc/adb/adb_keys
Man-in-the-Middle Protection
ADB uses RSA key exchange to prevent MitM attacks. The fingerprint shown in the authorization dialog should match the connecting computer.
Implementation Reference
File : core/device_manager.py
Key Methods :
__init__(): Initialize manager (line 30)
_get_adb_path(): Locate ADB executable (line 37)
is_installed(): Check ADB availability (line 50)
download_adb(): Auto-download platform tools (line 53)
run_command(): Execute ADB commands (line 68)
get_devices(): List connected devices (line 83)
get_detailed_device_info(): Query device properties (line 92)
check_packages(): Verify app installation (line 155)
find_backups(): Scan for backup files (line 163)
dump_backup(): Pull single file (line 238)
dump_media_with_progress(): Pull directory with progress (line 254)
Dependencies :
subprocess: Command execution
requests: Download platform-tools
zipfile: Extract downloaded archives
tqdm: Progress bars for large transfers
Troubleshooting Solutions for ADB connection issues and common errors
Device Setup Enabling USB debugging and device authorization