Hide a Phone Call Completely in iOS (Jailbreak Device)

Hide a phone call completely in iOS (jailbreak device)

I will try to post as much code as I can but it will not work from scratch. I use my own macroses to generate hooks so you have to rewrite them to work with your code. I will use pseudo function IsHiddenCall to determine if a given call is our hidden call (simple phone number check). It's here to simplify the code. You obviously have to implement it yourself. There will be other pseudo functions but their implementation is very simple and will be obvious from their names. It's not a simple tweak so bear with me.

Also, the code is non-ARC.

Basically, we hook everything that might tell iOS that there is a phone call.

iOS 7

Let's start with iOS 7 as it's the last version of iOS right now and hidden call implementation is simpler than on iOS 6 and below.

Almost everything we need is located in private TelephonyUtilities.framework. In iOS 7 Apple moved almost everything related to phone calls in that framework. That's why it got simpler - all other iOS components use that framework so we only need to hook it once without the need to poke around in every iOS daemon, framework that might do something with phone calls.

All methods are hooked in two processes - SpringBoard and MobilePhone (phone application). Bundle IDs are com.apple.springboard and com.apple.mobilephone, respectively.

Here is the list of TelephonyUtilities.framework methods I hook in both processes.

//TUTelephonyCall -(id)initWithCall:(CTCallRef)call
//Here we return nil in case of a hidden call. That way iOS will ignore it
//as it checks for nil return value.
InsertHookA(id, TUTelephonyCall, initWithCall, CTCallRef call)
{
if (IsHiddenCall(call) == YES)
{
return nil;
}

return CallOriginalA(TUTelephonyCall, initWithCall, call);
}

//TUCallCenter -(void)handleCallerIDChanged:(TUTelephonyCall*)call
//This is CoreTelephony notification handler. We ignore it in case of a hidden call.
//call==nil check is required because of our other hooks that might return
//nil object. Passing nil to original implementation might break something.
InsertHookA(void, TUCallCenter, handleCallerIDChanged, TUTelephonyCall* call)
{
if (call == nil || IsHiddenCall([call destinationID]) == YES)
{
return;
}

CallOriginalA(TUCallCenter, handleCallerIDChanged, call);
}

//TUCallCenter +(id)callForCTCall:(CTCallRef)call;
//Just like TUTelephonyCall -(id)initWithCall:(CTCallRef)call
InsertHookA(id, TUCallCenter, callForCTCall, CTCallRef call)
{
if (IsHiddenCall(call) == YES)
{
return nil;
}

return CallOriginalA(TUCallCenter, callForCTCall, call);
}

//TUCallCenter -(void)disconnectAllCalls
//Here we disconnect every call there is except our hidden call.
//This is required in case of a hidden conference call with hidden call.
//Our call will stay hidden but active while other call is active. This method is called
//when disconnect button is called - we don't wont it to cancel our hidden call
InsertHook(void, TUCallCenter, disconnectAllCalls)
{
DisconnectAllExceptHiddenCall();
}

//TUCallCenter -(void)disconnectCurrentCallAndActivateHeld
//Just like TUCallCenter -(void)disconnectAllCalls
InsertHook(void, TUCallCenter, disconnectCurrentCallAndActivateHeld)
{
DisconnectAllExceptHiddenCall();
}

//TUCallCenter -(int)currentCallCount
//Here we return current calls count minus our hidden call
InsertHook(int, TUCallCenter, currentCallCount)
{
return CallOriginal(TUCallCenter, currentCallCount) - GetHiddenCallsCount();
}

//TUCallCenter -(NSArray*)conferenceParticipantCalls
//Hide our call from conference participants
InsertHook(id, TUCallCenter, conferenceParticipantCalls)
{
NSArray* calls = CallOriginal(TUCallCenter, conferenceParticipantCalls);

BOOL isThereHiddenCall = NO;
NSMutableArray* callsWithoutHiddenCall = [NSMutableArray array];
for (id i in calls)
{
if (IsHiddenCall([i destinationID]) == NO)
{
[callsWithoutHiddenCall addObject:i];
}
else
{
isThereHiddenCall = YES;
}
}

if (callsWithoutHiddenCall.count != calls.count)
{
//If there is only two calls - hidden call and normal - there shouldn't be any sign of a conference call
if (callsWithoutHiddenCall.count == 1 && isThereHiddenCall == YES)
{
[callsWithoutHiddenCall removeAllObjects];
}
[self setConferenceParticipantCalls:callsWithoutHiddenCall];
[self _postConferenceParticipantsChanged];
}
else
{
return calls;
}
}

//TUTelephonyCall -(BOOL)isConferenced
//Hide conference call in case of two calls - our hidden and normal
InsertHook(BOOL, TUTelephonyCall, isConferenced)
{
if (CTGetCurrentCallCount() > 1)
{
if (CTGetCurrentCallCount() > 2)
{
//There is at least two normal calls - let iOS do it's work
return CallOriginal(TUTelephonyCall, isConferenced);
}

if (IsHiddenCallExists() == YES)
{
//There is hidden call and one normal call - conference call should be hidden
return NO;
}
}

return CallOriginal(TUTelephonyCall, isConferenced);
}

//TUCallCenter -(void)handleCallStatusChanged:(TUTelephonyCall*)call userInfo:(id)userInfo
//Call status changes handler. We ignore all events except those
//that we marked with special key in userInfo object. Here we answer hidden call, setup
//audio routing and doing other stuff. Our hidden call is indeed hidden,
//iOS doesn't know about it and don't even setup audio routes. "AVController" is a global variable.
InsertHookAA(void, TUCallCenter, handleCallStatusChanged, userInfo, TUTelephonyCall* call, id userInfo)
{
//'call' is nil when this is a hidden call event that we should ignore
if (call == nil)
{
return;
}

//Detecting special key that tells us that we should process this hidden call event
if ([[userInfo objectForKey:@"HiddenCall"] boolValue] == YES)
{
if (CTCallGetStatus(call) == kCTCallStatusIncoming)
{
CTCallAnswer(call);
}
else if (CTCallGetStatus(call) == kCTCallStatusActive)
{
//Setting up audio routing
[AVController release];
AVController = [[objc_getClass("AVController") alloc] init];
SetupAVController(AVController);
}
else if (CTCallGetStatus(call) == kCTCallStatusHanged)
{
NSArray *calls = CTCopyCurrentCalls(nil);
for (CTCallRef call in calls)
{
CTCallResume(call);
}
[calls release];

if (CTGetCurrentCallCount() == 0)
{
//No calls left - destroying audio controller
[AVController release];
AVController = nil;
}
}

return;
}
else if (IsHiddenCall([call destinationID]) == YES)
{
return;
}

CallOriginalAA(TUCallCenter, handleCallStatusChanged, userInfo, call, userInfo);
}

Here is Foundation.framework method I hook in both processes.

//In iOS 7 telephony events are sent through local NSNotificationCenter. Here we suppress all hidden call notifications.
InsertHookAAA(void, NSNotificationCenter, postNotificationName, object, userInfo, NSString* name, id object, NSDictionary* userInfo)
{
if ([name isEqualToString:@"TUCallCenterControlFailureNotification"] == YES || [name isEqualToString:@"TUCallCenterCauseCodeNotification"] == YES)
{
//'object' usually holds TUCall object. If 'object' is nil it indicates that these notifications are about hidden call and should be suppressed
if (object == nil)
{
return;
}
}

//Suppressing if something goes through
if ([object isKindOfClass:objc_getClass("TUTelephonyCall")] == YES && IsHiddenCall([object destinationID]) == YES)
{
return;
}

CallOriginalAAA(NSNotificationCenter, postNotificationName, object, userInfo, name, object, userInfo);
}

Here is the last method I hook in both processes from CoreTelephony.framwork

//CTCall +(id)callForCTCallRef:(CTCallRef)call
//Return nil in case of hidden call
InsertHookA(id, CTCall, callForCTCallRef, CTCallRef call)
{
if (IsHiddenCall(call) == YES)
{
return nil;
}

return CallOriginalA(CTCall, callForCTCallRef, call);
}

Here is SetupAVController function I used earlier. Hidden call is trully hidden - iOS doesn't know anything about it so when we answer it audio routing is not done and we will not hear anything on the other end. SetupAVController does this - it setups audio routing like iOS does when there is active phone call. I use private APIs from private Celestial.framework

extern id AVController_PickableRoutesAttribute;
extern id AVController_AudioCategoryAttribute;
extern id AVController_PickedRouteAttribute;
extern id AVController_AllowGaplessTransitionsAttribute;
extern id AVController_ClientPriorityAttribute;
extern id AVController_ClientNameAttribute;
extern id AVController_WantsVolumeChangesWhenPaused;

void SetupAVController(id controller)
{
[controller setAttribute:[NSNumber numberWithInt:10] forKey:AVController_ClientPriorityAttribute error:NULL];
[controller setAttribute:@"Phone" forKey:AVController_ClientNameAttribute error:NULL];
[controller setAttribute:[NSNumber numberWithBool:YES] forKey:AVController_WantsVolumeChangesWhenPaused error:NULL];
[controller setAttribute:[NSNumber numberWithBool:YES] forKey:AVController_AllowGaplessTransitionsAttribute error:NULL];
[controller setAttribute:@"PhoneCall" forKey:AVController_AudioCategoryAttribute error:NULL];
}

Here is method I hook only in MobilePhone process

/*
PHRecentCall -(id)initWithCTCall:(CTCallRef)call
Here we hide hidden call from call history. Doing it in MobilePhone
will hide our call even if we were in MobilePhone application when hidden call
was disconnected. We not only delete it from the database but also prevent UI from
showing it.
*/
InsertHookA(id, PHRecentCall, initWithCTCall, CTCallRef call)
{
if (call == NULL)
{
return CallOriginalA(PHRecentCall, initWithCTCall, call);
}

if (IsHiddenCall(call) == YES)
{
//Delete call from call history
CTCallDeleteFromCallHistory(call);

//Update MobilePhone app UI
id PHRecentsViewController = [[[[[UIApplication sharedApplication] delegate] rootViewController] tabBarViewController] recentsViewController];
if ([PHRecentsViewController isViewLoaded])
{
[PHRecentsViewController resetCachedIndexes];
[PHRecentsViewController _reloadTableViewAndNavigationBar];
}
}

return CallOriginalA(PHRecentCall, initWithCTCall, call);
}

Methods I hook in SpringBoard process.

//SpringBoard -(void)_updateRejectedInputSettingsForInCallState:(char)state isOutgoing:(char)outgoing triggeredbyRouteWillChangeToReceiverNotification:(char)triggered
//Here we disable proximity sensor
InsertHookAAA(void, SpringBoard, _updateRejectedInputSettingsForInCallState, isOutgoing, triggeredbyRouteWillChangeToReceiverNotification, char state, char outgoing, char triggered)
{
CallOriginalAAA(SpringBoard, _updateRejectedInputSettingsForInCallState, isOutgoing, triggeredbyRouteWillChangeToReceiverNotification, state, outgoing, triggered);

if (IsHiddenCallExists() == YES && CTGetCurrentCallCount() == 1)
{
BKSHIDServicesRequestProximityDetectionMode = (void (*)(int))dlsym(RTLD_SELF, "BKSHIDServicesRequestProximityDetectionMode");
BKSHIDServicesRequestProximityDetectionMode(0);
}
}

//BBServer -(void)publishBulletin:(id)bulletin destinations:(unsigned int)destinations alwaysToLockScreen:(char)toLockScreen
//Suppress hidden call bulletins
InsertHookAAA(void, BBServer, publishBulletin, destinations, alwaysToLockScreen, id bulletin, unsigned int destinations, char toLockScreen)
{
if ([[bulletin section] isEqualToString:@"com.apple.mobilephone"] == YES)
{
NSArray *recordTypeComponents = [[bulletin recordID] componentsSeparatedByString:@" "];
NSString *recordType = recordTypeComponents[0];
NSString *recordCode = recordTypeComponents[1];

//Missed call bulletin
if ([recordType isEqualToString:@"missedcall"] == YES)
{
NSArray *recordCodeComponents = [recordCode componentsSeparatedByString:@"-"];
NSString *phoneNumber = recordCodeComponents[0];

if (IsHiddenCall(phoneNumber) == YES)
{
return;
}
}
}

CallOriginalAAA(BBServer, publishBulletin, destinations, alwaysToLockScreen, bulletin, destinations, toLockScreen);
}

//TUCallCenter -(id)init
//CoreTelephony notifications handler setup
InsertHook(id, TUCallCenter, init)
{
CTTelephonyCenterAddObserver(CTTelephonyCenterGetDefault(), self, CallStatusNotificationCallback, kCTCallStatusChangeNotification, NULL, CFNotificationSuspensionBehaviorDeliverImmediately);

return CallOriginal(TUCallCenter, init);
}

//Call status changes handler. Here we redirect status changes into hooked TUCallCenter method and doing some other stuff.
void CallStatusNotificationCallback(CFNotificationCenterRef center, void* observer, CFStringRef name, const void* object, CFDictionaryRef userInfo)
{
if (object == NULL)
{
return;
}

if (IsHiddenCall((CTCallRef)object) == YES)
{
[observer handleCallStatusChanged:object userInfo:[NSDictionary dictionaryWithObject:[NSNumber numberWithBool:YES] forKey:@"HiddenCall"]];
}
else
{
if (CTCallGetStatus((CTCallRef)object) == kCTCallStatusHanged)
{
if (IsHiddenCallExists() == YES)
{
//Setting up all the audio routing again. When normal call is hanged iOS may break audio routing as it doesn't know there is another active call exists (hidden call)
SetupAVController(AVController);
}
else if (CTGetCurrentCallCount() == 0)
{
[AVController release];
AVController = nil;
}
}
}

if (CTGetCurrentCallCount() > 1 && IsHiddenCallExists() == YES)
{
//Here we setup hidden conference call
NSArray *calls = CTCopyCurrentCalls(nil);
for (CTCallRef call in calls)
{
CTCallJoinConference(call);
}
[calls release];
}
}

iOS 5-6

iOS 5-6 is more complex. Telephony code is scattered arount many iOS components and APIs. I might post the code later as I don't have time right now. The answer is already really long.

Disconnect or connect an iPhone call programmatically

There is much better place to do that - MPTelephonyManager -(void)displayAlertForCall:(id)call. This method is located in IncomingCall.servicebundle binary, not in SpringBoard itself. This binary is being loaded at runtime into SpringBoard when there is an incoming call. Before that IncomingCall.servicebundle is not loaded thus you can't hook it's methods.

Hooking IncomingCall.servicebundle

In order to hook the method, first, read this How to hook methods of MPIncomingPhoneCallController? SBPluginManager is loading *.servicebundle binaries at runtime. You need to hook it's -(Class)loadPluginBundle:(id)bundle method. It's gonna look something like this:

void displayAlertForCall_hooked(id self, SEL _cmd, id arg1);
void(*displayAlertForCall_orig)(id, SEL, id) = NULL;

%hook SBPluginManager
-(Class)loadPluginBundle:(NSBundle*)bundle
{
Class ret = %orig;

if ([[bundle bundleIdentifier] isEqualToString:@"com.apple.mobilephone.incomingcall"] && [bundle isLoaded])
{
MSHookMessageEx(objc_getClass("MPTelephonyManager"),
@selector(displayAlertForCall:),
(IMP)displayAlertForCall_hooked,
(IMP*)&displayAlertForCall_orig);
}

return ret;
}
%end

As you can see, hooking is deferred until IncomingCall.servicebundle is loaded. I don't know logos/theos that well but I think it can't do that. That's why I used CydiaSubstrate (MobileSubstrate) API.

Hooking MPTelephonyManager

#define SYSTEM_VERSION_LESS_THAN(v) ([[[UIDevice currentDevice] systemVersion] compare:v options:NSNumericSearch] == NSOrderedAscending)
typedef void* CTCallRef;
void CTCallDisconnect(CTCallRef);
void CTCallAnswer(CTCallRef);

void displayAlertForCall_hooked(id self, SEL _cmd, id arg1)
{
CTCallRef call = NULL;
if (SYSTEM_VERSION_LESS_THAN(@"7.0"))
{
//On iOS 6 and below arg1 has CTCallRef type
call = arg1;
}
else
{
//On iOS 7 arg1 has TUTelephonyCall* type
call = [arg1 call];
}

NSString *callNumber = (NSString*)CFBridgingRelease(CTCallCopyAddress(NULL, call));
if ([callNumber isEqualToString:@"+98.........."])
{
CTCallAnswer(call);
//CTCallDisconnect(call);
}

%orig;
}

How to intercept adding calls to call history in iOS?

There is no such function. At least in iOS7.

I've posted solution for iOS7 here Hide a phone call completely in iOS (jailbreak device)

Here is the code:

//Private API from CoreTelephony.framework
void CTCallDeleteFromCallHistory(CTCallRef call);

%hook PHRecentCall

-(id)initWithCTCall:(CTCallRef)call
{
if (IsCallShouldBeDeleted(call) == YES)
{
//Delete call from call history
CTCallDeleteFromCallHistory(call);

//Update MobilePhone app UI
id PHRecentsViewController = [[[[[UIApplication sharedApplication] delegate] rootViewController] tabBarViewController] recentsViewController];
if ([PHRecentsViewController isViewLoaded])
{
[PHRecentsViewController resetCachedIndexes];
[PHRecentsViewController _reloadTableViewAndNavigationBar];
}

//Try uncommenting this, may be it will work. Should make the code faster.
//return nil;
}

return %orig;
}

%end

Tweak hooks class inside MobilePhone app so bundle filter is com.apple.mobilephone.

IsCallShouldBeDeleted is pseudo function that determines whether a call should be deleted. You can remove it or implement your own. It's there just to make the code more clear.

On iOS6 class names are different but code is exactly the same - Apple just renamed the classes. I use that solution since iOS4. Also on iOS4 it requires a bit more code as there was no CTCallDeleteFromCallHistory function.

How do I detect that an iOS app is running on a jailbroken phone?

It depends what you mean by jailbreak. In the simple case, you should be able to see if Cydia is installed and go by that - something like

NSString *filePath = @"/Applications/Cydia.app";
if ([[NSFileManager defaultManager] fileExistsAtPath:filePath])
{
// do something useful
}

For hacked kernels, it's a little (lot) more involved.



Related Topics



Leave a reply



Submit