MIPS ROP-system parameter issue

Catalpa 网络安全爱好者

MIPS 架构下的栈溢出安全缺陷 exploit - system 函数参数覆盖问题。

常见的 MIPS 下安全缺陷利用方式

MIPS 架构下的栈溢出 exploit 通常会使用 ret2shellcode、ret2text、ret2libc 三种方式。

ret2shellcode

MIPS 指令集中存在一类特殊的 gadget,它们可以通过 $sp 寄存器寻址,将栈地址($sp + offset)加载到某寄存器中,然后跳转到该寄存器执行。另外,MIPS 架构下仅有受限的 no-execute 保护[1],根据 MIPS 官方文档[2]描述,只有在 MIPS Release 3 以及 SmartMIPS ASE 中包含类似 no-execute 的 XI(Execute Inhibit) 保护,而大量采用 MIPS CPU 的设备中,stack 内存区域通常包含 rwx 权限。

基于以上两点,我们可以将 shellcode 布置于 stack,然后利用此类 gadget 跳转到 shellcode 执行。

ret2text

某些情况下包含安全缺陷的目标程序中含有比较有用的代码片段,例如程序中包含调用 system 函数的逻辑,此时可通过 ret2text 返回到程序内部的 system 函数执行命令。

ret2libc

以上两种方式基本都需要多次控制程序执行流程,然而在实际情况下,很多包含安全缺陷的程序对于用户提交的数据有限制,例如 \x00 在 web 服务中会截断其后的数据,导致请求不完整。此类情况下会考虑将程序执行流劫持到 libc 中,因为 libc 地址较高,不会包含 \x00 等坏字节。ret2libc 的一种经典思路是将命令保存到 stack,然后通过 ROP 将 stack 地址加载至 $a0 寄存器,最后跳转到 libc 中的 system 函数执行命令。

以上是 MIPS 架构中比较常用的安全缺陷利用方式,最终达到执行系统命令的目的(详细思路可参考互联网文章)。

system 参数覆盖问题

下面简单说明什么是 system 参数覆盖以及这个问题是如何出现的。

假设一网络设备的 web 服务中包含一个栈溢出安全缺陷,当用户发送的请求中 URI 部分过长,会导致程序向栈缓冲区拷贝过多的数据,使程序崩溃。

按照经典的 exploit 思路,这里可以采用 ret2libc 方式,先用 mipsrop 工具从 libc 中查找合适的 gadget

1
|  0x000518D0  |  addiu $a0,$sp,24 ; jr    68($sp)

我们看到位于 0x518D0 位置的 gadget 比较合适,它将 $sp + 24 地址加载到 $a0,然后跳转到 $sp + 68 指向的地址继续执行。

所以可进行如下内存布局:

padding 部分填充栈空间数据,gadget_address(libc_address + 0x000518D0)覆盖了返回地址,当程序从包含安全缺陷的函数返回时,由于此函数为非叶子函数,所以会从栈上获取返回地址加载到 $ra,而返回地址已经被我们修改为 gadget,所以程序跳转到 gadget 继续执行。

这段 gadget 具体的汇编代码序列如下

1
2
3
4
5
6
7
8
addiu   $a0, $sp, 24
lw $ra, 68($sp)
lw $s3, 64($sp)
lw $s2, 60($sp)
lw $s1, 56($sp)
lw $s0, 52($sp)
jr $ra
addiu $sp, 72

执行时,$sp + 24 地址被加载到 $a0,$sp + 68 地址的值被加载到 $ra,然后以 4 字节为单位将 $sp + 64 ~ $sp + 52 的数据加载到 $s3 ~ $s0,最后用 jr 指令跳转到 $ra 即 $sp + 68 指向的内存区域,由于 MIPS 的流水线效应,最后一句 addiu $sp,72 在跳转之后已经执行完毕。

此时几个寄存器状态:

1
2
3
4
5
6
7
8
$a0 = command
$ra = system_address
$s0 = "aaaa"
$s1 = "aaaa"
$s2 = "aaaa"
$s3 = "aaaa"
$pc = system_address
$sp = old_sp + 72

由于最后的 addiu $sp, 72,$sp 指向我们布局的内存数据之下(即 data 部分):

$pc 已经指向 system 函数,程序继续执行 system 对应代码,查看 system 函数开头的部分汇编代码

1
2
3
4
5
6
7
8
9
la      $gp, locret_1E4F0  # Alternative name is '__libc_system'
addu $gp, $t9
addiu $sp, -40
sw $s0, 24($sp)
la $s0, loc_50000
sw $gp, 16($sp)
sw $s1, 28($sp)
sw $ra, 36($sp)
sw $s2, 32($sp)

其中第三条指令为 system 函数分配栈空间,把 $sp 减去 40,得到 system 函数的栈顶,后续 system 以及 system 所调用的子函数,都会使用这个栈顶及以上的内存来存取临时变量,如图

实际上小于 $sp + 72 的栈空间都可能会被其它变量的数据所覆盖。

如果恰巧有某个临时变量的数据被保存到 $sp + 24 附近的内存,本应是正常的系统命令就会被破坏,导致 exploit 失败。

如何避免此类问题

方法1:寻找包含 jr $sx 的 gadget 或利用其它寄存器转移参数

mipsrop 中可能会找到如下 gadget

1
|  0x00016BC8  |  addiu $s0,$sp,16 ; jalr  $s1  

这个 gadget 对应的指令序列如下

1
2
3
4
5
addiu   $s0, $sp, 16
move $a0, $s0
move $t9, $s1
jalr $t9
move $a1, $s5

先将 $sp + 16 地址加载到 $s0,然后 $s0 被赋值给 $a0,最后跳转到 $s1,期间不存在对 $sp 的修改。这种 gadget 很多情况下可以防止 command 被覆盖。

方法2:利用 popen 部分代码

popen 中调用了 execl 函数来执行系统命令,其中参数加载过程通常位于同一个汇编代码块中

1
2
3
4
5
6
7
8
9
10
la      $a0, loc_50000
la $a1, loc_50000
la $a2, loc_50000
la $t9, execl
addiu $a0, (aBinSh - 0x50000) # "/bin/sh"
sw $zero, 0x50+var_40($sp)
addiu $a1, (off_533C4 - 0x50000)
addiu $a2, (off_532C0 - 0x50000)
bal execl
move $a3, $s7

$a0、$a1、$a2 在代码中都已完成加载,我们只需要控制 $s7 为 command 地址,这样在跳转到 execl 执行前,move $a3,$s7 就会把 command 地址放到 $a3 中,完成参数传递。

通过 mipsrop 找到将 stack 地址加载到 $s7 的 gadget

1
2
3
4
5
6
7
8
9
addiu   $s7, $sp, 24
addiu $s6, 0x101
addiu $s5, (asc_54958 - 0x50000)
lw $a0, -22144($fp)
move $a1, $s7
move $a2, $s6
move $t9, $s4
jalr $t9
move $a3, $s5

加载后跳转到 $s4 继续执行,但如果直接跳回 popen,会导致参数异常,这是因为 MIPS 中对全局变量寻址有特殊的策略,MIPS 为了方便对全局变量数据的寻址,引入一个叫做 $gp 的寄存器,通过此寄存器,一条 4 字节的指令即可实现在 $gp +- 32K 内存中的寻址过程[3]。

$gp 是在函数开头就设置好的,由于我们是直接跳转到 popen 函数的中间部分,这就导致 $gp 错误,无法正确找到相关参数,所以需要引入另一段 gadget 来修正 $gp。

$gp 寄存器具体的值可通过调试目标函数开头确定,这个值相对 libc 基址偏移固定。确定好 $gp 值后即可寻找相关 gadget 修正。

1
2
3
4
lw      $gp, 16($sp)
move $t9, $s1
jalr $t9
li $a0, 3

以上 gadget 会将 $sp + 16 的值加载到 $gp,然后跳转到 $s1,恰好可以满足需求。

综合上述流程,gadget 执行顺序如下

1
2
3
4
将 $sp + 24 填充为 command,跳转到第一段 gadget ($ra)
gadget1: 将 command 地址加载到 $s7,跳转到第二段 gadget ($s4)
gadget2: 将 $sp + 16 加载到 $gp,跳转到第三段 gadget ($s1)
gadget3: popen 部分代码

这样执行 popen 部分时,各参数可正确寻址,并且 command 也不会被覆盖,可以成功执行系统命令。

实际上,除了 MIPS 外其他架构也可能存在类似问题,主要原因就是需要使用的数据位于下一个函数的栈帧内,比较通用的思路,一是可以调整数据位置,将其移动到不会被覆盖或修改的位置,或者可以利用对应指令集的特性来避免数据被覆盖。

参考文章

[1] operating system - MIPS memory execution prevention - Stack Overflow

[2] MIPS® Architecture For Programmers

[3] MIPS $gp register - Stack Overflow

  • Title: MIPS ROP-system parameter issue
  • Author: Catalpa
  • Created at : 2021-09-17 00:00:00
  • Updated at : 2024-10-17 08:51:27
  • Link: https://wzt.ac.cn/2021/09/17/mipsrop/
  • License: This work is licensed under CC BY-SA 4.0.