逆向角度分析 CydiaSubstrate Hook 原理

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://blog.csdn.net/u013702935/article/details/20143513
简介

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

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

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



准备工作

我们用Xcode创建一个空目标工程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 <fopen+0>: bx pc
0x3a043696 <fopen+2>: nop   (mov r8, r8)
0x3a043698 <fopen+4>: blx 0x3a4480d8
0x3a04369c <fopen+8>: ldr r1, [r1, #120]
0x3a04369e <fopen+10>: lsls r1, r1, #0
0x3a0436a0 <fopen+12>: mov r8, r0
0x3a0436a2 <fopen+14>: mov r0, r1
0x3a0436a4 <fopen+16>: mov r1, r2
0x3a0436a6 <fopen+18>: blx 0x3a0969ac <dyld_stub___sflags>
0x3a0436aa <fopen+22>: mov r5, r0
0x3a0436ac <fopen+24>: movs r6, #0
0x3a0436ae <fopen+26>: cmp r5, #0
0x3a0436b0 <fopen+28>: beq.n 0x3a04374e <fopen+186>
0x3a0436b2 <fopen+30>: movs r0, #1
0x3a0436b4 <fopen+32>: blx 0x3a09698c <dyld_stub___sfp>
其实fopen的函数入口的前n个字节已经被修改,下面是未Hook的fopen的前n字节。

代码:
0x3a043694 <fopen+0>: push {r4, r5, r6, r7, lr}
0x3a043696 <fopen+2>: add r7, sp, #12
0x3a043698 <fopen+4>: str.w r8, [sp, #-4]!
0x3a04369c <fopen+8>: sub sp, #4
0x3a04369e <fopen+10>: mov r2, sp
0x3a0436a0 <fopen+12>: mov r8, r0
0x3a0436a2 <fopen+14>: mov r0, r1
0x3a0436a4 <fopen+16>: mov r1, r2
0x3a0436a6 <fopen+18>: blx 0x3a0969ac <dyld_stub___sflags>
0x3a0436aa <fopen+22>: mov r5, r0
0x3a0436ac <fopen+24>: movs r6, #0
0x3a0436ae <fopen+26>: cmp r5, #0
0x3a0436b0 <fopen+28>: beq.n 0x3a04374e <fopen+186>
0x3a0436b2 <fopen+30>: movs r0, #1
0x3a0436b4 <fopen+32>: blx 0x3a09698c <dyld_stub___sfp>
可以比较两段汇编代码发现,一直到<fopen+12>才开始匹配。那么前12个字节的汇编代码,一定就是我们之前所说的跳转代码(改变寄存器PC的值)。

进行逐行分析:

代码:
0x3a043694 <fopen+0>: bx pc 
bx带状态切换的无条件跳转,因为ARM流水线,PC目前指向当前地址+4的位置。

代码:
0x3a043698 <fopen+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 <fopen+0>: 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 <my_fopen+0>: movw r2, #120 ; 0x78
0x00083f8c <my_fopen+4>: movt r2, #0 ; 0x0
0x00083f90 <my_fopen+8>: add r2, pc
0x00083f92 <my_fopen+10>: ldr r2, [r2, #0]
0x00083f94 <my_fopen+12>: ldr r2, [r2, #0]
0x00083f96 <my_fopen+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 <fopen+12>: mov r8, r0
其实不然,一个函数的调用周期关系到堆栈平衡的问题,在原函数入口和出口,进行了对堆栈平衡的操作

代码:
0x3a043694 <fopen+0>: push {r4, r5, r6, r7, lr}
....
0x3a043756 <fopen+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] <Warning>: 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 <fopen+0>: push {r4, r5, r6, r7, lr}
0x3a043696 <fopen+2>: add r7, sp, #12
0x3a043698 <fopen+4>: str.w r8, [sp, #-4]!
0x3a04369c <fopen+8>: sub sp, #4
0x3a04369e <fopen+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 <fopen+13>: mov r8, r0
0x3a0436a3 <fopen+15>: mov r0, r1
0x3a0436a5 <fopen+17>: mov r1, r2
0x3a0436a7 <fopen+19>: blx 0x3a0969ac <dyld_stub___sflags>
0x3a0436ab <fopen+23>: mov r5, r0
0x3a0436ad <fopen+25>: movs r6, #0
0x3a0436af <fopen+27>: cmp r5, #0
0x3a0436b1 <fopen+29>: beq.n 0x3a04374e <fopen+186>
0x3a0436b3 <fopen+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




转载请注明文章出处:http://www.rainyx.com/post/148.html
展开阅读全文

没有更多推荐了,返回首页