移除SIM卡弹框

前言

  • 目的:如果iPhone没有插入SIM卡,每次重启或注销后,重新进入桌面都会弹出如下框。每次点击,弹框才会消除,才能正常地使用iPhone,这样的体验也太差了。
  • 环境:iPhone6、iOS11、unc0ver越狱
  • 工具:
    • Cycript可以用来探索、修改、调试正在运行的iOS APP
    • IDA静态分析二进制文件
    • LLDB与DebugServer动态调试
    • Theos插件开发

从视图到控制器

  • 注入SpringBoard,查看详细的层级结构。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
      iPhoneName:~ root# cycript -p SpringBoard
    cy# UIApp.keyWindow.recursiveDescription().toString()
    ...省略很多...
    | | <_UIAlertControllerView: 0x12c043600; frame = (52.5 281.5; 270 104.5); layer = <CALayer: 0x1c4a2ede0>>
    | | | <UIView: 0x12fb85c90; frame = (0 0; 270 104.5); layer = <CALayer: 0x1c483bb40>>
    | | | | <_UIAlertControllerInterfaceActionGroupView: 0x12fd3c4f0; frame = (0 0; 270 104.5); opaque = NO; gestureRecognizers = <NSArray: 0x1c56450a0>; layer = <CALayer: 0x1c4a2ee60>>
    | | | | | <_UIDimmingKnockoutBackdropView: 0x12fd28600; frame = (0 0; 270 104.5); clipsToBounds = YES; layer = <CALayer: 0x1c4633e20>>
    | | | | | | <UIView: 0x12fd2b5f0; frame = (0 0; 270 104.5); clipsToBounds = YES; layer = <CALayer: 0x1c4634040>>
    | | | | | | <UIVisualEffectView: 0x12fd29330; frame = (0 0; 270 104.5); clipsToBounds = YES; layer = <CALayer: 0x1c4633d60>>
    | | | | | | | <_UIVisualEffectBackdropView: 0x12fd2b3e0; frame = (0 0; 270 104.5); autoresize = W+H; userInteractionEnabled = NO; layer = <UICABackdropLayer: 0x1c4633dc0>>
    | | | | | | | <_UIVisualEffectSubview: 0x12fd36f00; frame = (0 0; 270 104.5); autoresize = W+H; userInteractionEnabled = NO; layer = <CALayer: 0x1c4634000>>
    | | | | | <UIView: 0x12fd35cb0; frame = (0 0; 270 104.5); clipsToBounds = YES; layer = <CALayer: 0x1c4633800>>
    | | | | | | <_UIInterfaceActionGroupHeaderScrollView: 0x12ca8c600; frame = (0 0; 270 60); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x1c1647da0>; layer = <CALayer: 0x1c0e3af20>; contentOffset: {0, 0}; contentSize: {270, 60}; adjustedContentInset: {0, 0, 0, 0}>
    | | | | | | | <UIView: 0x12fcbc9e0; frame = (0 0; 270 60); layer = <CALayer: 0x1c0a3eea0>>
    | | | | | | | | <UILabel: 0x12fcb98e0; frame = (16 20; 238 20.5); text = '\u672a\u5b89\u88c5 SIM \u5361'; userInteractionEnabled = NO; layer = <_UILabelLayer: 0x1c0a82f30>>
    ...省略很多...
    ```
    * 从输入内容可以发现`\u672a\u5b89\u88c5 SIM \u5361`,对这个字符串进行Unicode解码,发现得到的内容和提示内容一致。
    ```shell
    $ echo "\u672a\u5b89\u88c5 SIM \u5361"
    未安装 SIM 卡
  • 从上可知,0x12fcb98e0就是我们需要的目标视图,顺着它的响应链一直不停地往上寻找(一直调用nextResponder方法),就可以找到对应的控制器_SBAlertController
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    cy# #0x12fcb98e0.nextResponder
    #"<UIView: 0x12fcbc9e0; frame = (0 0; 270 60); layer = <CALayer: 0x1c0a3eea0>>"
    cy# #0x12fcb98e0.nextResponder.nextResponder
    #"<_UIInterfaceActionGroupHeaderScrollView: 0x12ca8c600; frame = (0 0; 270 60); clipsToBounds = YES; gestureRecognizers = <NSArray: 0x1c1647da0>; layer = <CALayer: 0x1c0e3af20>; contentOffset: {0, 0}; contentSize: {270, 60}; adjustedContentInset: {0, 0, 0, 0}>"
    cy# #0x12fcb98e0.nextResponder.nextResponder.nextResponder
    #"<UIView: 0x12fd35cb0; frame = (0 0; 270 104.5); clipsToBounds = YES; layer = <CALayer: 0x1c4633800>>"
    cy# #0x12fcb98e0.nextResponder.nextResponder.nextResponder.nextResponder
    #"<_UIAlertControllerInterfaceActionGroupView: 0x12fd3c4f0; frame = (0 0; 270 104.5); opaque = NO; gestureRecognizers = <NSArray: 0x1c56450a0>; layer = <CALayer: 0x1c4a2ee60>>"
    cy# #0x12fcb98e0.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder
    #"<UIView: 0x12fb85c90; frame = (0 0; 270 104.5); layer = <CALayer: 0x1c483bb40>>"
    cy# #0x12fcb98e0.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder
    #"<_UIAlertControllerView: 0x12c043600; frame = (52.5 281.5; 270 104.5); layer = <CALayer: 0x1c4a2ede0>>"
    cy# #0x12fcb98e0.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder.nextResponder
    #"<_SBAlertController: 0x12cb8da00; title: \"\xe6\x9c\xaa\xe5\xae\x89\xe8\xa3\x85 SIM \xe5\x8d\xa1\">"
  • _SBAlertController就是我们寻找的目标控制器,通过打印发现继承于UIAlertController控制器。
    1
    2
    cy# [#0x12cb8da00 superclass]
    UIAlertController
  • 如果有iOS开发经验的同学就会知道这个弹框的伪代码大致如下所示。我们接下来就要寻找那里执行的这段代码,也就是说执行这段代码的条件。让条件不成立,就不会有弹框了
    1
    2
    3
    4
    5
    UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"未安装SIM卡" message:nil preferredStyle: UIAlertControllerStyleAlert];
    UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"好" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
    }];
    [alert addAction: okAction];
    [self presentViewController:alert animated:YES completion:nil];

定位核心代码

  • iPhone上面开启debugserver
    1
    2
    root# killall -9 SpringBoard
    root# debugserver 127.0.0.1:10011 -a SpringBoard
  • macOS上使用LLDB链接SpringBoard,并给UIAlertController所有方法断点
    • 连接:(lldb) process connect connect://127.0.0.1:10011
    • 设置:(lldb) br set -r ‘[UIAlertController .*]‘
  • 输入c继续程序,断点命中-[UIAlertController initWithNibName:bundle:]
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    (lldb) c
    Process 3303 resuming
    Process 3303 stopped
    thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.2
    frame #0: 0x000000018a8ab9f8 UIKit` -[UIAlertController initWithNibName:bundle:]
    UIKit`-[UIAlertController initWithNibName:bundle:]:
    -> 0x18a8ab9f8 <+0>: sub sp, sp, #0x40 ; =0x40
    0x18a8ab9fc <+4>: stp x22, x21, [sp, #0x10]
    0x18a8aba00 <+8>: stp x20, x19, [sp, #0x20]
    0x18a8aba04 <+12>: stp x29, x30, [sp, #0x30]
    0x18a8aba08 <+16>: add x29, sp, #0x30 ; =0x30
    0x18a8aba0c <+20>: mov x19, x3
    0x18a8aba10 <+24>: mov x20, x0
    0x18a8aba14 <+28>: mov x0, x2
    Target 0: (SpringBoard) stopped.
  • 使用bt查看调用堆栈。Foundation和CoreFoundation都属于系统框架,这样的调用先不管。选择SpringBoard调用最上层作为线索,开始追踪。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    (lldb) bt
    * thread #1, queue = 'com.apple.main-thread', stop reason = breakpoint 1.2
    * frame #0: 0x000000018a8ab9f8 UIKit` -[UIAlertController initWithNibName:bundle:]
    frame #1: 0x0000000197180a00 SpringBoardUI` -[SBAlertItem _prepareNewAlertControllerWithLockedState:requirePasscodeForActions:] + 72
    frame #2: 0x0000000100e3f428 SpringBoard` ___lldb_unnamed_symbol22251$$SpringBoard + 108
    frame #3: 0x0000000100c66b10 SpringBoard` ___lldb_unnamed_symbol10676$$SpringBoard + 200
    frame #4: 0x0000000100bc4bd4 SpringBoard` ___lldb_unnamed_symbol6876$$SpringBoard + 668
    frame #5: 0x0000000100bc3960 SpringBoard` ___lldb_unnamed_symbol6858$$SpringBoard + 1484
    frame #6: 0x0000000100bc337c SpringBoard` ___lldb_unnamed_symbol6857$$SpringBoard + 160
    frame #7: 0x0000000100bc2c40 SpringBoard` ___lldb_unnamed_symbol6842$$SpringBoard + 292
    frame #8: 0x0000000100bc8f6c SpringBoard` ___lldb_unnamed_symbol6958$$SpringBoard + 1872
    frame #9: 0x000000018194b4c4 Foundation` __NSFireDelayedPerform + 412
    frame #10: 0x0000000180f1a92c CoreFoundation` __CFRUNLOOP_IS_CALLING_OUT_TO_A_TIMER_CALLBACK_FUNCTION__ + 28
    frame #11: 0x0000000180f1a650 CoreFoundation` __CFRunLoopDoTimer + 864
    frame #12: 0x0000000180f19e50 CoreFoundation` __CFRunLoopDoTimers + 248
    frame #13: 0x0000000180f17a38 CoreFoundation` __CFRunLoopRun + 1928
    frame #14: 0x0000000180e37fb8 CoreFoundation` CFRunLoopRunSpecific + 436
    frame #15: 0x0000000182ccff84 GraphicsServices` GSEventRunModal + 100
    frame #16: 0x000000018a40c2e8 UIKit` UIApplicationMain + 208
    frame #17: 0x0000000196828570 FrontBoard` FBSystemAppMain + 1096
    frame #18: 0x000000018095a56c libdyld.dylib` start + 4
  • 找到SpringBoard的Mach-O文件,拖进去IDA,进行静态分析
    1
    2
    3
    4
    5
    iPhoneName:~ root# ps -A | grep SpringBoard
    3303 ?? 0:08.70 /System/Library/CoreServices/SpringBoard.app/SpringBoard
    3305 ttys000 0:03.47 /usr/bin/debugserver 127.0.0.1:10011 -a SpringBoard
    3318 ttys001 0:00.01 grep SpringBoard
    iPhoneName:~ root#
  • 使用image lookup -a查看0x0000000100bc8f6c在Macho-O文件中的位置。
    1
    2
    3
    (lldb) image lookup -a 0x0000000100bc8f6c
    Address: SpringBoard[0x0000000100120f6c] (SpringBoard.__TEXT.__text + 1163848)
    Summary: SpringBoard`___lldb_unnamed_symbol6958$$SpringBoard + 1872
  • IDA中按G,输入0x0000000100120f6c,定位到代码相应的位置,按Fn + F5查看伪代码如下
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    ...省略很多...
    if ( !v62 && !((v49 | (v38 != 0)) & 1) && !v4->_lockEntryAlert )
    {
    v65 = v4->_status;
    if ( (v65 | 2) != 2 )
    {
    v66 = ((id (__cdecl *)(SBSIMLockAlertItem_meta *, SEL, signed __int64))objc_msgSend)(
    (SBSIMLockAlertItem_meta *)&OBJC_CLASS___SBSIMLockAlertItem,
    "alertTitleForStatus:",
    v65);
    v67 = objc_retainAutoreleasedReturnValue(v66);
    objc_release(v67);
    if ( v67 )
    {
    v68 = (SBSIMLockAlertItem *)objc_msgSend(&OBJC_CLASS___SBSIMLockAlertItem, "alloc");
    v69 = (SBSIMLockAlertItem *)-[SBSIMLockAlertItem initWithStatus:](v68, "initWithStatus:", v4->_status);
    v70 = objc_msgSend(&OBJC_CLASS___SBAlertItemsController, v5);
    v71 = (void *)objc_retainAutoreleasedReturnValue(v70);
    objc_msgSend(v71, "activateAlertItem:", v69);
    objc_release(v71);
    v72 = objc_storeWeak(v90, v69);
    v73 = sub_10048325C(v72);
    v74 = objc_retainAutoreleasedReturnValue(v73);
    if ( (unsigned int)os_log_type_enabled() )
    {
    HIWORD(v83) = 256;
    v84 = 0;
    v85 = 8;
    v86 = v69;
    _os_log_impl(&_mh_execute_header, v74, 1LL, aSlmPoppingAler, *(_QWORD *)((char *)&v83 + 2));
    }
    objc_release(v74);
    objc_release(v69);
    }
    }
    }
    ...省略很多...

分析核心代码

  • 0x0000000100120f6c实际定位到objc_release(v71);行,往上看,发现三个if判断条件:
    • if ( v67 )用来判断有没有获取到相应的提示文字。
    • if ( (v65 | 2) != 2 )判断_status的取值,根据取值判断要不要执行下面的代码
    • if ( !v62 && !((v49 | (v38 != 0)) & 1) && !v4->_lockEntryAlert )判断条件太多,先不分析。
  • 重点分析_status的取值
    • 上下文可以判断核心代码位于-[SBSIMLockManager _updateToStatus:]方法中,v4的值为SBSIMLockManager
    • v65 = v4->_status;等价于v65 = SBSIMLockManager->_status
    • 假设让v4->_status = 0,那么(v65 | 2) != 2 就不成立,不会执行下面的代码,就不会有弹框。
  • 使用class-dump导出SpringBoard所有的头文件。打开SBSIMLockManager.h,发现
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    @interface SBSIMLockManager : NSObject
    {
    _Bool _isBrickedDevice;
    long long _status;
    SBSIMLockAlertItem *_weak_currentAlert;
    SBSIMLockEntryAlert *_lockEntryAlert;
    NSString *_languageCode;
    _Bool _hasHadSIMWhileNotBricked;
    _Bool _wasActivated;
    _Bool _neededUIM;
    }
    ...省略很多...
    - (long long)status; // IMP=0x0000000100121a1c
    • 可以看出status只有getter方法和成员变量,没有setter方法。
    • status只可读不可写,不能通过setter方法设置_status的值,需要找到直接给_status成员变量赋值的代码。
  • 光标移动到v65 = v4->_status;后面,按tab键切换到汇编模式下,内容如下:
    1
    0000000100120EE4                 LDR             X2, [X19,X27]
  • LLDB中给0000000100120EE4下地址断点,命中断点后,打印x19和x27的值。从结果可以发现_status的值是SBSIMLockManager偏移了16个字节。
    1
    2
    3
    4
    5
    <SBSIMLockManager: 0x1c427eb00>
    (lldb) po $x19
    <SBSIMLockManager: 0x1c427eb00>
    (lldb) po $x27
    16
  • -[SBSIMLockManager _updateToStatus:]最开始的地方设置地址断点。断点命中后,打印SBSIMLockManager的地址,用这个地址加上16个字节,就是_status的地址。
    1
    2
    (lldb) po $x0
    <SBSIMLockManager: 0x1c546af80>
  • _status设置一个观察点,观察点被命中的时候我们就知道_status被改变了。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    (lldb) watchpoint set expression (0x1c546af80+0x10)
    Watchpoint created: Watchpoint 1: addr = 0x1c546af90 size = 8 state = enabled type = w
    new value: 0
    (lldb) c
    Process 3864 resuming

    Watchpoint 1 hit:
    old value: 0
    new value: 3
    Process 3864 stopped
    thread #1, queue = 'com.apple.main-thread', stop reason = watchpoint 1
    frame #0: 0x0000000100aecb8c SpringBoard` ___lldb_unnamed_symbol6958$$SpringBoard + 880
    SpringBoard`___lldb_unnamed_symbol6958$$SpringBoard:
    -> 0x100aecb8c <+880>: cmp x24, #0x4 ; =0x4
    0x100aecb90 <+884>: b.hi 0x100aecc8c ; <+1136>
    0x100aecb94 <+888>: cmp x24, #0x2 ; =0x2
    ...省略很多...
    Target 0: (SpringBoard) stopped.
  • 内存观察点设置后,按c过掉断点,发现值被改了。断点停留在0x100aecb8c位置上,使用image lookup -a 查找在IDA中的位置,如下。
    1
    2
    3
    (lldb) image lookup -a 0x100aecb8c
    Address: SpringBoard[0x0000000100120b8c] (SpringBoard.__TEXT.__text + 1162856)
    Summary: SpringBoard`___lldb_unnamed_symbol6958$$SpringBoard + 880
  • IDA中按G,输入0x0000000100120b8c,定位到如下位置,发现_status是由v3赋值得到的。
    1
    2
    3
    v4->_isBrickedDevice = v12;
    v4->_status = v3;
    if ( v3 > 4 || v3 == 2 )
  • 根据上下文可以得出:v3就是-[SBSIMLockManager _updateToStatus:]方法的第一个参数,所以改第一个参数就相当于更改了_status的值。
    1
    2
    3
    4
    5
    void __cdecl -[SBSIMLockManager _updateToStatus:](SBSIMLockManager *self, SEL a2, signed __int64 a3) {
    ...省略很多...
    v3 = a3;
    ...省略很多...
    }
  • 利用Theos新建Tweak项目,MobileSubstrate Bundle filter填写com.apple.springboard。新建完成后,在Tweak.x填写如下内容
    1
    2
    3
    4
    5
    %hook SBSIMLockManager
    - (void)_updateToStatus:(long long)arg1 {
    %orig(0);
    }
    %end
  • 进入Tweak项目目录,执行make && make package && make install安装插件,安装完成后,自动注销桌面。重新进入桌面后,发现弹框提示没有了,任务就完成了

想要更多

  • 以上已经解决了移除SIM卡弹框的问题,但是想探究更多的方法,于是有了以下内容。
  • 重新查看SBSIMLockManager.h,从名字和方法可以看出是个单例类,猜想是用来管理SIM卡的。可以看到跟status有关的方法。_updateToStatus已经分析过了,这里着重分析_statusFromCT_CTToSBSIMStatus:
    1
    2
    3
    - (long long)_statusFromCT;
    - (long long)_CTToSBSIMStatus:(struct __CFString *)arg1;
    - (void)_updateToStatus:(long long)arg1
  • 使用br set -a- (long long)_CTToSBSIMStatus:(struct __CFString *)arg1;下地址断点。
    • 断点命中后po $x2值为kCTSIMSupportSIMStatusNotInserted为没有插卡状态,所以才会有未插卡的弹框。
    • 有开发经验的同学应该知道SIM卡有四种状态:
      1
      2
      3
      4
      extern NSString* const kCTSMSMessageReceivedNotification;
      extern NSString* const kCTSMSMessageReplaceReceivedNotification;
      extern NSString* const kCTSIMSupportSIMStatusNotInserted;
      extern NSString* const kCTSIMSupportSIMStatusReady;
    • 其中CTSIMSupportSIMStatusReady为有卡时候的状态,为了不弹框,我们设置为有卡状态。
      1
      2
      3
      4
      5
      %hook SBSIMLockManager
      - (long long)_CTToSBSIMStatus:(NSString *)arg1 {
      return %orig(@"kCTSIMSupportSIMStatusReady");
      }
      %end
  • 同样地分析 - (long long)_statusFromCT;
    • IDA打开加载SpringBoard二进制文件,查看方法实现的伪代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      signed __int64 __cdecl -[SBSIMLockManager _statusFromCT](SBSIMLockManager *self, SEL a2)
      {
      SBSIMLockManager *v2; // x19
      struct objc_object *v3; // x0
      void *v4; // x20
      void *v5; // x21

      v2 = self;
      v3 = +[SBTelephonyManager sharedTelephonyManager](&OBJC_CLASS___SBTelephonyManager, "sharedTelephonyManager");
      v4 = (void *)objc_retainAutoreleasedReturnValue(v3);
      v5 = objc_msgSend(v4, "SIMStatus");
      objc_release(v4);
      return (signed __int64)-[SBSIMLockManager _CTToSBSIMStatus:](v2, "_CTToSBSIMStatus:", v5);
      }
    • 还原以上伪代码如下
      1
      return [SBSIMLockManager _CTToSBSIMStatus: [[SBTelephonyManager sharedTelephonyManager] SIMStatus]
    • 更改SBTelephonyManager SIMStatus的返回值就可以从源头改变状态值,代码如下
      1
      2
      3
      4
      5
      %hook SBTelephonyManager
      - (id)SIMStatus {
      return @"kCTSIMSupportSIMStatusReady";
      }
      %end
  • 综上所述,上面三种方法选择其中一种就可以了。