3

I want to hide a phone call completely in ios. My priority is to do this on ios 7 (latest ios version at this time!) but i would like to know how to hide a phone call on ios 6 and below too if possible. I have found some functions to do so as hooking into initWithAlertController method of class SBUIFullscreenAlertAdapter. Thanks to creker in this link I found another method to hook that is better to do so. The problem is it still has a callbar when the phone is not locked or when the phone is locked the phone shows that it's it in the middle of communication. Here are screenshots: link to image

I want to know what are the methods dealing with this to hook? Is there anything else that i should know for achieving what i want?

For deleting any other traces that are left i thought of after the call is finished i delete the call history from it's database. Is there a better way?

Community
  • 1
  • 1
iFred
  • 41
  • 1
  • 5

1 Answers1

6

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.

creker
  • 9,400
  • 1
  • 30
  • 47
  • Thank u very much. Your answer helped me a lot. I have tested it and I have some problems. 1- There is an error here:`extern id AVController_PickableRoutesAttribute;` and the similar lines, it says it can't find it. what is the problem? 2- `DisconnectAllExceptGoodCall()` what should it do? I mean which call is a good call? – iFred Apr 09 '14 at 09:37
  • For other functions like `DisconnectAllExceptHiddenCall()` i used `CTCopyCurrentCalls(nil)` to access all calls and then used `CTCallDisconnect(call)` for each object of it. Am i doing it right? – iFred Apr 09 '14 at 09:53
  • When you say hooking in springboard process do you mean setting bundle filter to `com.apple.springboard` and with MobilePhone do you mean `com.apple.mobilephone`? Can i build all of these in one tweak or should there be three? (one for MobilePhone process, one for SpringBoard process and one for both?) – iFred Apr 09 '14 at 09:56
  • 1. `DisconnectAllExceptGoodCall` should be `DisconnectAllExceptHiddenCall`, I renamed it. 2. `AVController_PickableRoutesAttribute` - did you link the framework? 3. `DisconnectAllExceptHiddenCall` should disconnect all calls except our hidden call. 4. You could write one tweak for everything. In fact, that's how I do it. – creker Apr 09 '14 at 10:05
  • 2- I copied a class dump of Celestial framework to include folder of iOSopendev and imported Celestial.h. Should i do anything else? – iFred Apr 09 '14 at 10:43
  • You may need to link it to your project (drop it in "framework" folder in your project). I don't think Xcode can find private framework on it's own. – creker Apr 09 '14 at 11:00
  • In method `handleCallStatusChanged` of class `TUCallCenter` there is a line `CTCallAnswer(call)`. `call`'s type is `TUTelephonyCall*` but `CTCallAnswer`'s argument should be `CTCallRef`. How is it possible? In some other places you have `IsHiddenCall(TUTelephonyCall*)` and `IsHiddenCall([TUTelephonyCall* destinationID])` how should i handle them? I have tried [TUTelephony* call] but it was wrong. thanks – iFred Apr 09 '14 at 20:13
  • See `CallStatusNotificationCallback`. There I call `handleCallStatusChanged` passing `CTCallRef` argument and `userInfo` argument with `HiddenCall` key. So the types are correct. I just reuse existing functions to do all the work in one place. As for `IsHiddenCall`, I didn't find `IsHiddenCall(TUTelephonyCall*)`. There are two cases - `IsHiddenCall(CTCallRef)` and `IsHiddenCall(NSString*)`. – creker Apr 09 '14 at 20:29
  • In the prototype of `handleCallStatusChanged`, `call` is `TUTelephonyCall*`. So why do u pass a CTCallRef? – iFred Apr 10 '14 at 05:32
  • Does it matter? `call` argument is a pointer, I can pass whatever I want - method is hooked, calling it will call my own implementation. I wanted to move call status handling in one place for both processes. There may be other reasons but I don't remember them - it was more of a trial and error process. You don't have to do exactly what I did but my code works, it's tested many many times in different scenarious. – creker Apr 10 '14 at 07:31
  • Thank you creker! The hidden call is now working well except for the mobile phone app that has the call in the call history that i will try to figure it out myself. But now there is a problem with regular calls. In `handleCallStatusChanged` it crashes. I can't find where it happens but i think it's in the last if statement and the original function. If the call is regular, will `call`'s type be `TUTelephonyCall*` or is it still `CTCallRef`? – iFred Apr 10 '14 at 14:00
  • I am guessing it's gonna be `TUTelephonyCall*` because you are using `[call destinationID]`. What is the type of `destinationID`? my guess is `CTCallRef` – iFred Apr 10 '14 at 14:06
  • `destinationID` returns `NSString*` – creker Apr 10 '14 at 14:18
  • For regular calls `handleCallStatusChanged` argument is `TUTelephonyCall*`, I wrote correct method prototypes above all hooks. If I pass `CTCallRef` then original implementation is NOT called - there is `return` statement for that. – creker Apr 10 '14 at 14:26
  • Ok! the problem was that I thought `destinationID` returned a `CTCallRef`!!! Now it's better. I have a question and that is I can't hook in `PHRecentCall`. I mean when I log inside `initWithCall` nothing is logged. Any ideas? (I am using `com.apple.mobilephone` and `com.apple.springboard` bundle filters plus a `Mode = "Any"` filter.) – iFred Apr 10 '14 at 19:52
  • Check method selector, bundle id filter. There is not much I can help with. Here is class-dump https://github.com/limneos/classdump-dyld/blob/master/iphoneheaders/iOS7.0.3/Applications/MobilePhone.app/PHRecentCall.h Maybe it will help. – creker Apr 10 '14 at 20:01
  • Hi again! It's a little bit late but there is a question. How could we set only the mic on and set the speaker off? Because if the phone is playing any sound it will be paused and the phone call will be on the speaker. – iFred May 10 '14 at 19:32
  • I don't know. Try, maybe it will work and will help somebody else too. – creker May 15 '14 at 10:58
  • I think I should change the attribute being set for `AVController_AudioCategoryAttribute` in `SetupAVController` function. What it is being set is `@"PhoneCall"` which I think it should be changed. The problem is I don't know what should I change it with!;) Can you help? – iFred May 15 '14 at 11:06
  • I don't know either. I got that `@"PhoneCall"` string from SpringBoard disassembly - that's how it sets up audio routing when there is an active phone call. – creker May 15 '14 at 11:23
  • awesome work @creker! about iOS 6, do you have the names of the methods to hook at least? or the frameworks which need to be hooked? – Alexandre Blin May 17 '14 at 08:35
  • @creker Although this is for long time ago but can you help me a little bit with the disassembly. I have tried to do some stuff but I can't get what I want. I have looked at the disassembly of the celestial framework but there are no strings checked at `setAttribute:forKey` method. So how can I reach the right attribute for my problem (that is to only setup the microphone and not the speaker) – iFred Jul 01 '14 at 11:14