MSFunction原理

CydiaSubstrate,iOS7越狱之前名为 MobileSubstrate(下文简称为MS或MS框架),作者为大名鼎鼎的Jay Freeman(saurik).

MS框架为越狱iDevice提供了一个稳定的代码修改平台。开发者可以很方便的利用它进行各种插件开发工作。可以说目前很多插件都是基于MS实现,比如activator、barrel、KuaiDial等等。

目前大部分人对MS的认知也只停留在表面(API Hook 应用),却很少有人探究它的原理。今天笔者就以逆向分析的角度,去一探究竟。

 

 

准备工作

我们用Xocde创建一个空目标工程mssheep.app,并设定它的Bundle ID为com.rainyx.mssheep,再把他生成到iPhone上,这样我们就可以通过com.rainyx.mssheep这个Bundle ID对目标程序mssheep进行注入了,当然前提是要有MS的支持;p.

 

 再创一个动态库工程mswolf、用于Hook目标程序mssheep。在iOSOpenDev生成的模版中可以看到,按照MS的规则生成了两个文件,分别是/Library/MobileSubstrate/DynamicLibraries/xxx.dylib和/Library/MobileSubstrate/DynamicLibraries/xxx.plist,打开Plist在filters中加入com.rainyx.mssheep这个Bundle ID,然后同样生成到iPhone上。

 

在iOS桌面启动mssheep.app。然后SSH到iOS,GDB attach到目标进程,打info sh查看加载的库,在列表中有mswolf.dylib,说明我们的动态库成功被目标进程加载,这时候想干什么坏事就都可以干了/:阴险。

 

修改mswolf工程加入Hook部分代码,此次我们Hook fopen这个API。

FILE  * ( *old_fopen ) ( const  char  *path ,  const  char *mode ) ;

FILE  *my_fopen ( const  char  *path ,  const  char  *mode )
{
     // 这里什么都不干
     return old_fopen (path , mode ) ;
}

void Initialize ( )
{
    MSHookFunction ( & fopen ,  &my_fopen ,  ( void ** ) &old_fopen ) ;
}

我们要让mswolf在目标程序加在后运行Initialize函数,需要在Build Settings的Other Link Flags中加入-init _Initialize选项.编译后生成到iPhone上,再次运行mssheep。

 

 

入口跳转分析

目标API fopen被Hook以后,会跳转到我们自己定义的函数入口,即my_fopen,在ARM体系中,由寄存器PC来控制程序的执行顺序,换句话说,寄存器PC保存了下一条要被执行的指令的地址。MS如何改变流程,看下反汇编代码便知。

SSH到iPhone,GDB附加到目标进程,打disassemble fopen,如下:

0x3a043694 + 0> :  bx pc
0x3a043696 + 2> :  nop    ( mov r8 , r8 )
0x3a043698 + 4> : blx  0x3a4480d8
0x3a04369c + 8> : ldr r1 ,  [r1 , # 120 ]
0x3a04369e + 10> : lsls r1 , r1 , # 0
0x3a0436a0 + 12> :  mov r8 , r0
0x3a0436a2 + 14> :  mov r0 , r1
0x3a0436a4 + 16> :  mov r1 , r2
0x3a0436a6 + 18> : blx  0x3a0969ac
0x3a0436aa + 22> :  mov r5 , r0
0x3a0436ac + 24> :  movs r6 , # 0
0x3a0436ae + 26> :  cmp r5 , # 0
0x3a0436b0 + 28> : beq .0x3a04374e + 186>
0x3a0436b2 + 30> :  movs r0 , # 1
0x3a0436b4 + 32> : blx  0x3a09698c

其实fopen的函数入口的前n个字节已经被修改,下面是未Hook的fopen的前n字节。

0x3a043694 + 0> :  push  {r4 , r5 , r6 , r7 , lr }
0x3a043696 + 2> :  add r7 ,  sp , # 12
0x3a043698 + 4> :  str .w r8 ,  [ sp , # - 4 ]!
0x3a04369c + 8> :  sub  sp , # 4
0x3a04369e + 10> :  mov r2 ,  sp
0x3a0436a0 + 12> :  mov r8 , r0
0x3a0436a2 + 14> :  mov r0 , r1
0x3a0436a4 + 16> :  mov r1 , r2
0x3a0436a6 + 18> : blx  0x3a0969ac
0x3a0436aa + 22> :  mov r5 , r0
0x3a0436ac + 24> :  movs r6 , # 0
0x3a0436ae + 26> :  cmp r5 , # 0
0x3a0436b0 + 28> : beq .0x3a04374e + 186>
0x3a0436b2 + 30> :  movs r0 , # 1
0x3a0436b4 + 32> : blx  0x3a09698c

可以比较两段汇编代码发现,一直到才开始匹配。那么前12个字节的会变代码,一定就是我们之前所说的跳转代码(改变寄存器PC的值)。

进行逐行分析:

0x3a043694 + 0> :  bx pc 

bx带状态切换的无条件跳转,因为ARM流水线,PC目前指向当前地址+4的位置。

0x3a043698 + 4> : blx  0x3a4480d8

blx带状态并返回的无条件跳转,此处地址为0x3a4480d8,用GDB反汇编一下此地址

(gdb) disassemble 0x3a4480d8 0x3a4480d8+20
Dump of assembler code from 0x3a4480d8 to 0x3a4480ec:

0x3a4480d8 : andeq r0 , r0 , r4
0x3a4480dc : bcc  0x3b592594
0x3a4480e0 : subcc r8 , r7 , r10 , asr # 1
0x3a4480e4 : subcc r3 , r9 , r7 ,  lsl # 14
0x3a4480e8 : andeq r0 , r0 , r2

End of assembler dump.

是一堆乱七八糟的指令,肯定是不能够运行的,那为什么会跳转到这个地址呢?别忘了
0x3a043694 : bx pc 
这个是带状态转换的跳转指令,已知fopen入口为Thumb指令状态,GDB译码的时候当然是按Thumb状态下进行译码的。由于已知目前是Thumb状态而且pc指向一个非Thumb状态的地址0x3a043698,所以执行到0x3a043698这个地址的时候,要按ARM32进行译码。

得出了以上结论,我们在看看0x3a043698在ARM32状态下是什么。

 

在GDB中,敲入以下命令强制改变反汇编器的译码方式。
set arm force-mode arm
但是很不幸我的GDB(1821)并不支持这条命令,那怎么办呢?GDB在无symbol的情况下译码是通过cpsr的t位来决定译码方式的,那么我们目前t位正好是0,也就是ARM32方式译码。

 

所以说找一块无symbol的内存地址,把0x3a043698内容写入该地址然后进行反汇编就可以了。为了简单我索性就把基址拿过来用(是不是太暴力了)。
复制后两条指令内容到目标地址:
(gdb) set *0x2c000 = *0x3a043698
(gdb) set *0x2c004 = *0x3a04369c
(gdb) disassemble 0x2c000 0x2c008
Dump of assembler code from 0x2c000 to 0x2c008:

0x0002c000 <_mh_execute_header + 0> : ldr pc ,  [pc ,# - 4 ]  ; 0x2c004 <_mh_execute_header+4>
0x0002c004 <_mh_execute_header + 4> : andeq r3 , r8 ,r9 ,  lsl # 31

End of assembler dump.

 

GDB打印出正确的指令
0x0002c000 <_mh_execute_header+0>: ldr pc, [pc, #-4] ; 0x2c004 <_mh_execute_header+4>
ldr指令从读取一个内存地址的值到寄存器中,此刻pc = *(pc – 4),也就是 pc = *0x0002c004,不出意外这里应该是我们my_fopen的函数入口地址。

 

用GDB看下:
(gdb) p/x *0x0002c004
$1 = 0x83f89
(gdb) disassemble 0x83f89          
Dump of assembler code for function my_fopen:

0x00083f88 + 0> : movw r2 , # 120  ; 0x78
0x00083f8c + 4> : movt r2 , # 0  ; 0x0
0x00083f90 + 8> :  add r2 , pc
0x00083f92 + 10> : ldr r2 ,  [r2 , # 0 ]
0x00083f94 + 12> : ldr r2 ,  [r2 , # 0 ]
0x00083f96 + 14> :  bx r2

End of assembler dump.

确实为我们自定义的函数my_fopen。

目前我们完成了前12字节的跳转分析。

休息一下,思考另外一个问题:
在我们的my_fopen函数中

FILE  *my_fopen ( const  char  *path ,  const  char  *mode )
{
     // 这里什么都不干
     return old_fopen (path , mode ) ;
}

调用了old_fopen函数,那么old_fopen指针应该指向哪里?

 

 

备份分析

有些人认为应该指向这个地址:

0x3a0436a0 + 12> :  mov r8 , r0

其实不然,一个函数的调用周期关系到堆栈平衡的问题,在原函数入口和出口,进行了对堆栈平衡的操作

0x3a043694 + 0> :  push  {r4 , r5 , r6 , r7 , lr }
....
0x3a043756 + 194> :  pop  {r4 , r5 , r6 , r7 , pc }

如果直接调用fopen的第12字节,即使能调用成功,pop指令也没有匹配的push指令执行。导致堆栈失衡,程序崩溃。换句话说,调用了一个“残废”的函数,你能确保它的结果是正确的吗?除非你做了它“残废”后做不了的事儿(实际上也就是如此,不过我们还是来印证一下)。

 

还是来看一下MS到底把old_fopen指针指向哪里吧,
在mswolf工程的Initialize中稍加修改:

void Initialize ( )
{
    MSHookFunction ( & fopen ,  &my_fopen ,  ( void ** ) &old_fopen ) ;
    NSLog (@ "Pointer is 0x%x" , old_fopen ) ;
}

编译生成到iPhone,重新运行mssheep。查看日志:

Feb 27 14:15:45 xPhone mssheep[851] : Pointer is 0×76001

地址为:0×76001,用GDB附加查看。

(gdb) disassemble 0×76001 0×76001+50
Dump of assembler code from 0×76001 to 0×76033:

0x00076001 :  push  {r4 , r5 , r6 , r7 , lr }
0x00076003 :  add r7 ,  sp , # 12
0x00076005 :  str .w r8 ,  [ sp , # - 4 ]!
0x00076009 :  sub  sp , # 4
0x0007600b :  mov r2 ,  sp
0x0007600d :  bx pc
0x0007600f :  nop    ( mov r8 , r8 )
0x00076011 : blx  0x47aa50
0x00076015 : adds r6 , # 161
0x00076017 : subs r2 , # 4

看前12字节

0x00076001 :  push  {r4 , r5 , r6 , r7 , lr }
0x00076003 :  add r7 ,  sp , # 12
0x00076005 :  str .w r8 ,  [ sp , # - 4 ]!
0x00076009 :  sub  sp , # 4
0x0007600b :  mov r2 ,  sp

有的朋友一下就看出是怎么回事了,没错,这12字节就是fopen被替换的那前12字节:

0x3a043694 + 0> :  push  {r4 , r5 , r6 , r7 , lr }
0x3a043696 + 2> :  add r7 ,  sp , # 12
0x3a043698 + 4> :  str .w r8 ,  [ sp , # - 4 ]!
0x3a04369c + 8> :  sub  sp , # 4
0x3a04369e + 10> :  mov r2 ,  sp

再看后12字节:

0x0007600d :  bx pc
0x0007600f :  nop    ( mov r8 , r8 )
0x00076011 : blx  0x47aa50
0x00076015 : adds r6 , # 161

上面我们分析的fopen入口调转如出一辙,这里就不再赘述。0×00076015-1这个地址的内容应该就是原fopen+12的地址。
看下便知:
(gdb) p/x *(0×00076015-1)
$1 = 0x3a0436a1
(gdb) disassemble 0x3a0436a1 0x3a0436a1+20
Dump of assembler code from 0x3a0436a1 to 0x3a0436b5:

0x3a0436a1 + 13> :  mov r8 , r0
0x3a0436a3 + 15> :  mov r0 , r1
0x3a0436a5 + 17> :  mov r1 , r2
0x3a0436a7 + 19> : blx  0x3a0969ac
0x3a0436ab + 23> :  mov r5 , r0
0x3a0436ad + 25> :  movs r6 , # 0
0x3a0436af + 27> :  cmp r5 , # 0
0x3a0436b1 + 29> : beq .0x3a04374e + 186>
0x3a0436b3 + 31> :  movs r0 , # 1

End of assembler dump.

如此一来,我们在调用原fopen的功能前,先经过0×76001,恢复被修改的代码,然后再跳转到原fopen函数。这样的“拼接”完成了一次完整的调用,也就不会存在堆栈失衡的情况,程序保持正常运行。

 

总结

至此我们完成了整个Hook原理的分析,完成Hook功能,要做的事情大致有两件
1.修改目标函数前N字节,跳转到自定义函数入口;
2.备份目标函数前N个字节,跳转回目标函数。

此文分析的是在Thumb状态下进行的Hook,其实ARM32、ARM64我想原理也基本一样,只是指令和寻址方式有所不同。有兴趣的朋友可以研究一下!

所用设备:
1.Mac book pro
2.iPhone5(A1429)

所用工具:
1.SSH
2.GDB(1821) for iOS
3.Xcode 5.0 with iOSOpenDev

你可能感兴趣的:(ios逆向编程)