BIND_TELECOM_CONNECTION_SERVICE¶
System permission that allows an app to register a ConnectionService with Android's telecom framework. When combined with the default dialer role, this grants full control over incoming and outgoing calls: the app can intercept, redirect, answer, and fabricate calls while displaying fake caller ID. FakeCalls pioneered this attack surface for voice phishing (vishing), redirecting victims' bank calls to attacker-operated call centers.
Technical Details¶
| Attribute | Value |
|---|---|
| Permission | android.permission.BIND_TELECOM_CONNECTION_SERVICE |
| Protection Level | signature |
| Grant Method | App must register a ConnectionService; full call control requires becoming the default dialer via RoleManager or Settings |
| Introduced | API 23 (Android 6.0) |
| User Visibility | User must set the app as default phone app |
| Related | BIND_CALL_REDIRECTION_SERVICE (Android 10+ call redirection), PROCESS_OUTGOING_CALLS (deprecated) |
The signature protection level means only the system can bind to the service. Third-party apps declare the service and request the default dialer role. The critical distinction: BIND_CALL_REDIRECTION_SERVICE only intercepts outgoing calls, while a default dialer with ConnectionService controls the entire call lifecycle including incoming calls, call UI, and caller ID display.
What It Enables¶
ConnectionService Registration¶
public class MaliciousConnectionService extends ConnectionService {
@Override
public Connection onCreateIncomingConnection(PhoneAccountHandle handle, ConnectionRequest request) {
MaliciousConnection conn = new MaliciousConnection();
conn.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED);
conn.setRinging();
return conn;
}
@Override
public Connection onCreateOutgoingConnection(PhoneAccountHandle handle, ConnectionRequest request) {
String dialed = request.getAddress().getSchemeSpecificPart();
MaliciousConnection conn = new MaliciousConnection();
if (isBankNumber(dialed)) {
Uri attackerUri = Uri.fromParts("tel", ATTACKER_NUMBER, null);
conn.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED);
conn.setDialing();
routeToAttacker(conn, attackerUri);
} else {
conn.setAddress(request.getAddress(), TelecomManager.PRESENTATION_ALLOWED);
conn.setDialing();
}
return conn;
}
}
Default Dialer Role¶
RoleManager roleManager = getSystemService(RoleManager.class);
Intent intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER);
startActivityForResult(intent, REQUEST_DEFAULT_DIALER);
Becoming the default dialer grants:
| Capability | Effect |
|---|---|
| Outgoing call control | Intercept all outgoing calls, redirect to attacker numbers |
| Incoming call control | Answer, reject, or fabricate incoming calls |
| Call UI replacement | Display custom call screen showing spoofed caller ID |
| InCallService binding | Full control over the call UI and call state presentation |
| Call log access | Read and write call history |
Abuse in Malware¶
Voice Phishing (Vishing) via Default Dialer¶
The attack chain:
- Malware social-engineers the user into setting it as the default phone app ("enhanced spam protection" or "call blocking" feature)
- Victim later dials their bank's customer service number
- Malware's
ConnectionServiceintercepts the outgoing call - Call is silently redirected to an attacker-operated call center
- The malware's custom
InCallServicedisplays the bank's real number on screen - The victim sees their bank's number, hears a professional IVR, and provides credentials
- Attacker uses the credentials for account takeover
The victim has no visual indication the call was redirected. The phone screen shows the bank's name and number throughout the call.
FakeCalls / Letscall¶
FakeCalls is the defining malware family for this attack surface. First documented by Kaspersky in April 2022, it has evolved through multiple generations:
| Generation | Year | Capabilities | Source |
|---|---|---|---|
| v1 | 2022 | Outgoing call redirect to Korean banks, basic fake dialer UI | Kaspersky |
| v2 | 2023 | Incoming call spoofing, pre-recorded IVR audio, call recording | Check Point |
| v3 | 2024 | Bluetooth audio monitoring, screen state capture, Accessibility-based remote control, 13 apps identified | Zimperium |
FakeCalls targets South Korean banking customers exclusively. The malware impersonates apps from Kookmin Bank, KakaoBank, and other major Korean financial institutions. The attacker call centers employ Korean-speaking operators who follow scripts mimicking real bank customer service.
Bidirectional Call Control¶
Unlike BIND_CALL_REDIRECTION_SERVICE which only intercepts outgoing calls, the default dialer role enables bidirectional attacks:
| Direction | Attack |
|---|---|
| Outgoing | Victim calls bank, redirected to attacker |
| Incoming | Attacker calls victim, caller ID shows bank's number |
| Fabricated | Malware generates a fake "incoming call" from the bank with no real call |
The fabricated incoming call is particularly dangerous. The malware can create a Connection object and present it through the InCallService as a real incoming call, complete with ringtone and the bank's caller ID. No actual phone call occurs; the audio is pre-recorded IVR played locally or streamed from C2.
Comparison: Call Interception Methods¶
| Method | API | Direction | User Setup | Visibility |
|---|---|---|---|---|
BIND_TELECOM_CONNECTION_SERVICE + default dialer |
23+ | Both | Must become default phone app | Invisible (custom call UI) |
BIND_CALL_REDIRECTION_SERVICE |
29+ | Outgoing only | Must become default call redirect app | Invisible redirect |
PROCESS_OUTGOING_CALLS |
1-28 | Outgoing only | Runtime permission | Broadcast-based, deprecated |
ANSWER_PHONE_CALLS |
26+ | Incoming only | Runtime permission | Can auto-answer but not redirect |
The default dialer approach is the most powerful because it controls the entire call UI. The malware replaces the stock dialer, meaning it controls what the user sees during any call.
Android Version Changes¶
| Version | API | Change | Impact |
|---|---|---|---|
| 6.0 | 23 | ConnectionService and InCallService APIs introduced |
Apps can manage calls and replace the dialer UI |
| 7.0 | 24 | Default dialer role formalized | Clearer path for apps to become the phone app |
| 10 | 29 | RoleManager introduced for default apps |
ROLE_DIALER provides standardized default dialer request |
| 10 | 29 | CallRedirectionService added |
Alternative for outgoing-only redirection without default dialer role |
| 12 | 31 | Role-based permissions tightened | RoleManager enforces stricter default app validation |
| 14 | 34 | Restricted settings expanded | Sideloaded apps face additional barriers to becoming default dialer |
Detection Indicators¶
Manifest Signals¶
<service
android:name=".service.CallService"
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE">
<intent-filter>
<action android:name="android.telecom.ConnectionService" />
</intent-filter>
</service>
<service
android:name=".service.InCallUI"
android:permission="android.permission.BIND_INCALL_SERVICE">
<intent-filter>
<action android:name="android.telecom.InCallService" />
</intent-filter>
</service>
A ConnectionService + InCallService combination in a non-dialer app is a strong indicator of FakeCalls-type malware.
Behavioral Signals¶
- App requesting
ROLE_DIALERwithout legitimate dialer functionality - Call routing logic containing hardcoded bank phone numbers
- Pre-recorded audio files (
.mp3,.wav) with IVR-style content PhoneAccountHandleregistration followed by network communication to C2- Custom call screen UI mimicking the stock Android dialer or a specific bank app
Frida: Hook ConnectionService¶
Java.perform(function() {
var ConnectionService = Java.use("android.telecom.ConnectionService");
ConnectionService.onCreateOutgoingConnection.implementation = function(handle, request) {
var addr = request.getAddress();
console.log("[*] Outgoing call via ConnectionService: " + addr);
return this.onCreateOutgoingConnection(handle, request);
};
ConnectionService.onCreateIncomingConnection.implementation = function(handle, request) {
var addr = request.getAddress();
console.log("[*] Incoming call via ConnectionService: " + addr);
return this.onCreateIncomingConnection(handle, request);
};
});