异架构pwn
mips/arm pwn
环境安装
qemu安装
qemu 是一个支持跨平台虚拟化的虚拟机,有 user mode 和 system mode 两种配置方式。其中qemu 在system mode配置下模拟出整个计算机,可以在qemu之上运行一个操作系统。qemu 的system mode与常见的VMware和Virtualbox等虚拟机比较相似,但是qemu 的优势是可以跨指令集。例如,VMware和Virtualbox之类的工具通常只能在x86计算机上虚拟出一个x86计算机,而qemu 支持在x86上虚拟出一个ARM计算机。qemu在user mode配置下,可以运行跟当前平台指令集不同的平台可执行程序。例如可以用qemu在x86上运行ARM的可执行程序,但是两个平台必须是同一种操作系统,比如Linux。
1 | sudo apt install qemu |
gdb-multiarch
是一个经过交叉编译后的、支持多架构版本的 gdb。
1 | sudo apt install gdb-multiarch |
安装动态链接库
要安装对应架构的动态链接库,安装后会在/usr/目录下看到
有大佬已经给总结好了,好多稀奇古怪的架构
【Pwn 笔记】跨平台架构的环境配置与调试 | binLep‘s Blog
1 | # 具备MIPS交叉编译gcc与MIPS程序动态链接库 |
mips
初探
当下mips考点更多趋向于ret2txt,ret2libc,ret2shellcode也有堆的题目,解题时可以结合X86架构下的漏洞挖掘和利用思路
下载相应架构的lib
1 | $ apt search "libc6-" | grep "arm" |
ELF文件格式
这是linux下file生成的
1 | ELF 32-bit LSB executable, MIPS, MIPS32 rel2 version 1 (SYSV), dynamically linked, interpreter /lib/ld.so.1, for GNU/Linux 3.2.0, BuildID[sha1]=8e2e178dc761c3e8245a9742491867114e6757ab, not stripped |
含义
1 | 1. ELF 32-bit LSB executable |
分支延迟槽
MIPS架构有一个特殊的概念叫分支延迟槽。当程序遇到分支指令(如跳转指令)时,程序会跳转到新的地址去执行新指令。然而,由于流水线的设计,紧接在分支指令之后的指令已经在流水线中开始执行了。为了避免浪费,MIPS架构规定,分支后的第一条指令(即位于分支延迟槽中的指令)会在跳转之前执行。
这意味着,在编写MIPS汇编代码或分析MIPS的二进制文件时,需要特别注意分支延迟槽的存在。例如,在以下MIPS汇编代码中:
1 | .text:0007F944 move $t9, $s0 |
虽然jalr指令是一个跳转指令,但紧接在其后的move $a0, $s1指令会在跳转之前执行。
一般而言跳转指令的下一条指令会是nop,但这种行为在查找利用漏洞的gadgets以及构造payload时非常重要。
运行调试
本地运行
动态连接
导入lib文件
一般来说题目会提供程序依赖的so库,所以在程序根目录创建一个lib文件夹并将依赖文件放进去
文件目录结构

然后输入一下命令测试能不能运行
1 | qemu-mipsel -L ./ pwn |
静态连接
直接运行即可
gdb调试
直接调试
1 | qemu-mipsel -L /usr/mipsel-linux-gnu/ -g 6666 pwn #开启6666端口 |
脚本调试
我们在做题的时候肯定是在脚本中下断点更好调试
1 | #context.terminal = ['gnome-terminal','-x','sh','-c'] |
QAQ
调试的时候会遇到调用某个函数的时候进入某个libc地址
原因是远程调试过程中GDB加载了宿主机的库,为了阻止GDB查找宿主机上的库,需要设置GDB共享库查找路径前缀与共享库查找路径
1 | #gdb中设置 |
理论知识
MIPS(Microprocessor without Interlocked Pipeline Stages),是一种采取精简指令集(RISC)的指令集架构(ISA),最早的MIPS架构是32位,最新的版本已经变成64位。MIPS汇编语言是用于编写MIPS(Microprocessor without Interlocked Pipeline Stages)架构微处理器指令的低级语言,常见于路由器,MIPS架构是一种典型的精简指令集计算机(RISC)体系结构,最初由斯坦福大学的John Hennessy在1980年代初期设计,目的是为了实现高性能的处理器设计,通过使用简单的指令集来加速指令的执行速度。MIPS架构和汇编语言被广泛用于各种设备和应用中,包括嵌入式系统、路由器、微控制器以及视频游戏控制器等。
指令集
| REGISTER | NAME | USAGE |
|---|---|---|
| $0 | $zero | 存储常量 0 (constant value 0) |
| $1 | $at | 保留给汇编器 (Reserved for assembler) |
| $2-$3 | $v0-$v1 | 存放函数调用返回值或表达式 (values for results and expression evaluation) |
| $4-$7 | $a0-$a3 | 作为函数调用的前四个参数 (arguments) |
| $8-$15 | $t0-$t7 | 供汇编程序使用的临时寄存器 临时变量 |
| $16-$23 | $s0-$s7 | 调用子函数时 用于保存原寄存器的值 (saved) |
| $24-$25 | $t8-$t9 | 供汇编程序使用的临时寄存器 补充t0−t7 |
| $26-$27 | k0−k1 | 中断/异常处理程序使用 保存系统参数 |
| $28 | $gp | 全局指针 (Global Pointer) |
| $29 | $sp | 堆栈指针 (Stack Pointer) |
| $30 | $fp | 帧指针 (Frame Pointer) |
| $31 | $ra | 返回地址 (return address) |
- MIPS32架构函数调用方式
对栈的操作: MIPS32 架构堆栈与x86 架构的一样,都是从高地址向低地址增长的。但是在 MIPS 架构中没有 ebp/rbp 指针,在进入一个函数时,需要将当前栈顶指针上抬 N 字节,这个操作就是在给当前函数开辟它的栈帧,此后栈指针就不再移动,直至函数返回时重新加上 N 这个偏移,这个操作就是在恢复现场。由于不能随意移动栈指针,所以寄存器压栈和出栈时都必须指定偏移量
函数调用机制:
函数调用前,caller 函数会在自己的栈顶预留一部分空间来保存 callee 函数的参数,我们称之为调用参数空间;MIPS 的调用函数指令会将函数的返回地址直接存入 $RA 寄存器而不是堆中
函数调用时,前4个传入的参数通过 a0 a3 传递。有些函数的参数可能会超过4个,此时多余的参数会被放入调用参数空间
- MIPS32架构函数调用的栈布局
MIPS32 架构下的函数有两种类型——叶子函数和非叶子函数。==如果一个函数中不会再调用其他任何函数,那么这个函数就是一个叶子函数,其余的函数都是非叶子函数==
函数调用过程(函数 A 调用函数 B):
- 函数调用指令执行时会复制当前
$PC 寄存器的值到$RA 寄存器,即当前$RA 的值就是当前函数执行结束的返回地址,然后跳转到函数 B 执行代码 - 程序跳转到函数 B 后
如果函数 B 是非叶子函数,则函数 B 里首先会把函数 A 的返回地址入栈,函数返回时,函数 B 先从栈中取出被保存在栈上的返回地址,然后将返回地址存入寄存器$RA,再使用 “jr $RA” 指令返回函数 A
如果函数 B 是叶子函数,则在函数返回时直接使用 “jr $RA” 指令返回函数 A(个人感觉这一块能看出精简指令集的特性,对冗余指令进行优化,叶子函数没必要再入栈上层函数的返回地址)
函数调用参数传递:
MIPS 体系的函数调用,通过 a0 a3 来传递前4个参数,其余参数通过栈传递,栈帧结构一般如下

- 缓冲区溢出漏洞利用
在讨论完叶子函数和非叶子函数的栈帧结构差异后,就可以开始讨论它们各自的栈溢出漏洞利用的可行性了。非叶子函数会存储返回地址在栈中,所以与常规的栈溢出利用区别不大;然而若是在叶子函数中存在缓冲区溢出漏洞一般无法直接利用,除非存在可以溢出大量数据的情况,就可以通过一路覆盖到父函数中的返回地址去劫持程序流
例题
ret2win_mipsel
保护机制

如何确定溢出
需要控制$ra,程序最后都是有这一段gadget的,这里是ida反编译错误
精确计算的话:
0x38+var_s4=0x38+4=60
s=0x18=24
溢出所需字节:padding=60-24=36
0x38=56>padding溢出


在gdb中的格式如下
1 | ls $ra,0x3c($sp) |

返回地址一般也在fp上方

溢出点在这里,并且程序给了后门,没有gadget的话只能控制一次返回地址,前提是非子叶函数


直接溢出,控制返回地址为backdoor,getshell
1 | from pwn import* |
spilt
保护

padding=0x38+4-0x18=35,后门只有/bin/ls,但是是有/bin/cat flag.txt的字符串的
我们需要控制ra 和a0去执行system(/bin/cat flag.txt)
这时候就需要利用一段gadget



1 | ROPgadget --binary "./split_mipsel" | grep -E ": lw .*a0, .*sp" |
找到合适的gadget,可以控制a0还可以控制ra
网上还有一个找gadget的工具,适用于ida低版本,我使用的是9.1,不能使用该mipsrop
mipsrop
安装
https://bbs.kanxue.com/thread-266102.htm
直接看这个链接的评论区即可,然后直接把文件复制过去
常用的功能
mipsrop.find()
mipsrop.stackfinders()

接下来就是构造paylaod
a0=sp+8,ra=sp+4
1 | payload = b'a' * 36 |
最后跳转到后门,getflag
1 | from pwn import* |
Mplogin
题目给了libc文件,这就需要我们将目录改为 -L ./
.ds_store是macOS系统用来存储特定文件夹的自定义显示设置和元数据的文件,所以应该是这个文件对栈空间有些影响,所以我们直接把整个文件夹的内容复制过去,然后正常做题就可以

保护

题目



没有system函数,NX没开,考虑打shellcode。sub_400840中read有溢出,可以泄露出stack地址,sub_400978第一次read可以更改第二次size,造成栈溢出,栈溢出更改返回地址为shellcode
关于第一次read如何覆盖,这里ida中是不方便找的,可以结合调试快速发现

之后控制返回地址为我们的shellcode,getshell
1 | from pwn import* |
retlibc
题目
vuln中有栈溢出


保护

主要利用这两段gadget,s2-ra=>puts.plt s1-a0=>read.got
执行完这一段返回地址就被设置为vuln了,这里调试发现是执行puts时候的将返回地址设置为了vuln,具体什么原因后边在分析吧


返回到vuln有了第二次栈溢出的机会,已经拿到了libc的基地址,打system的rop链即可getshell


1 | from pwn import* |
[DASCTF X 0psu3十一月挑战赛|越艰巨·越狂热]shaopi(BUU)
静态编译,将符号表恢复一下如下,一开始有个base64检测,将解码好的输入一下


之后就有一个栈溢出了,静态编译,但没有符号表,去找gadget,可以跳转到fp上去执行shellcode

用下边两个gadget
1 | 0x0043965C | addiu $a2,$sp,0x68+var_10 | jalr $fp#将0x68+var_10地址赋值给a2 |
shellcraft自动生成的shellcode过长,0x74,网上找一个短的getshell
1 | from pwn import* |
缓存非一致性
哈佛结构
哈佛架构(Harvard architecture)是一种将程序指令储存和数据储存分开的存储器结构(Split Cache)。中央处理器首先到程序指令储存器中读取程序指令内容,解码后得到数据地址,再到相应的数据储存器中读取数据,并进行下一步的操作(通常是执行)。程序指令储存和数据储存分开,数据和指令的储存可以同时进行,可以使指令和数据有不同的数据宽度
CPU缓存(cache)
在计算机系统中,CPU 高速缓存是用于减少处理器访问内存所需平均时间的部件。当处理器发出内存访问请求时,会先查看缓存内是否有请求数据。如果存在(命中),则不经访问内存直接返回该数据;如果不存在(失效),则要先把内存中的相应数据载入缓存,再将其返回处理器。
为了和下级存储(如内存)保持数据一致性,就必须把数据更新适时传播下去。这种传播通过回写来完成。一般有两种回写策略:写回(Write back)和写通(Write through)。
写回是指,仅当一个缓存块需要被替换回内存时,才将其内容写入内存。如果缓存命中,则总是不用更新内存。为了减少内存写操作,缓存块通常还设有一个脏位(dirty bit),用以标识该块在被载入之后是否发生过更新。如果一个缓存块在被置换回内存之前从未被写入过,则可以免去回写操作。写回的优点是节省了大量的写操作。这主要是因为,对一个数据块内不同单元的更新仅需一次写操作即可完成。这种内存带宽上的节省进一步降低了能耗,因此颇适用于嵌入式系统。
写通是指,每当缓存接收到写数据指令,都直接将数据写回到内存。如果此数据地址也在缓存中,则必须同时更新缓存。由于这种设计会引发造成大量写内存操作,有必要设置一个缓冲来减少硬件冲突。这个缓冲称作写缓冲器(Write buffer),通常不超过4个缓存块大小。不过,出于同样的目的,写缓冲器也可以用于写回型缓存。
MIPS 的 Cache 机制
绝大部分 MIPS CPU 针对指令和数据有其各自的 cache(分别称为 Icache 和 Dcache),这样读一条指令和一个数据的读操作或者写操作就能同时发生。
- 处理器只能执行 Icache 中的指令,只能看到 Dcache 中的数据。一般来说,CPU 不能直接访问内存。
- 不能执行 DCache 中的指令,也不能编程读写 ICache 中的指令。
基于以上所知,我们在利用基于 MIPS 架构的栈溢出利用时,就会出现问题。具体表现如下:
我们的 ShellCode 会存到 Dcache 中,根据 写回 策略,内存中的数据一般不能得到及时更新。这导致我们的 Shellcode 无法进入 Icache,即无法执行,这就是所谓的 缓存非一致性 问题的由来。

解决方法也很简单,我们需要让 Dcache 中的数据写回主存,并让 Icache 中的指令失效,从主存中重新加载。
在 MIPS 架构中最简单的做法就是在执行 ShellCode 前构造 ROP 链调用 Sleep() 函数。
还可以调用cacheflush(系统调用)
- 刷出 D-Cache (Clean): 强行将 D-Cache 中
addr 到addr + nbytes 范围内的脏数据(即你的 Shellcode)写回到主物理内存中。 - 作废 I-Cache (Invalidate): 将 I-Cache 中对应地址范围的缓存行标记为无效(Invalid) 。
- 当 PC 指针再次跳到这段栈地址时,I-Cache 发现数据无效,被迫去主物理内存中重新加载。而此时的主内存中,已经是刚刚被 D-Cache写入的shellcode
arm
ELF文件格式
1 | chall: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.3, for GNU/Linux 3.2.0, BuildID[sha1]=829f6a8ec1a5969fb01d69b6fdc4053923e8736a, stripped |
运行调试
1 | sudo apt-get install gcc-arm-linux-gnueabi #arm |
理论知识
ARM 是 32 位的架构,AARCH64 是 64 位的架构
ARM 架构下的函数调用约定为,函数的前四个参数保存在寄存器 r0 - r3 中, 剩下的参数从右向左依次入栈, 被调用者实现栈平衡,函数的返回值保存在 r0 中
AARCH64 架构下的函数调用约定为,函数的前六个参数保存在寄存器 x0 - x5 中。与ARM架构不同的是,AARCH64 架构下,被调用者不需要实现栈平衡,由调用者负责栈平衡。函数的返回值保存在 x0 中,如果返回值是一个结构体或类似的较大的数据类型,则会使用 x0 和 x1 寄存器记录返回值
常见汇编
MOV: 数据传输指令,用于将数据从一个寄存器或内存中复制到另一个寄存器或内存中。
ADD/SUB: 加法和减法指令。
CMP: 比较指令,比较两个操作数的大小关系。
B: 分支指令,用于无条件跳转到指定地址。
BL: 分支指令,用于跳转到指定地址并将返回地址保存在LR寄存器中,可用于函数调用。
LDR/STR: 数据传输指令,用于从内存中读取数据或将数据写入内存。
- LDR 用于将某些内容从内存加载到寄存器中,例如
LDR R2, [R0] 从R0寄存器中存储的内存地址的值读入R2寄存器。 - STR 用于将某些内容从寄存器存储到内存地址中,例如
STR R2, [R1] 从R2寄存器中将值存储到R1寄存器中的内存地址中。
- LDR 用于将某些内容从内存加载到寄存器中,例如
PUSH/POP: 栈操作指令,用于将寄存器中的数据入栈或从栈中弹出数据。
根据做的题目来看,返回地址的位置不固定,这里需要结合调试去看,调试很重要!!!
QAQ
有时候调用shellcraft.aarch64.sh()的时候会报错,需要我们安装binutils依赖
1 | dpkg-query: 没有找到与 *bin/armeabi*linux*-as* 相匹配的路径 |
1 | apt search binutils | grep [arch]#aarch64为例 |

1 | sudo apt install binutils-aarch64-linux-gnu#安装即可 |
例题
typo(ret2text)
arm架构,静态编译,去除符号表
1 | typo: ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), statically linked, for GNU/Linux 2.6.32, BuildID[sha1]=211877f58b5a0e8774b8a3a72c83890f8cd38e63, stripped |
可以通过调试找到主函数

根据shift+F12追踪字符串,交叉引用找到main

代码也是很难看,这里我们可以尝试恢复一下符号表,但还是没办法看出来函数的主逻辑,之后直接调试来看

这里可以用cyclic -l来确定溢出值

因为我们没办法定为read的地址,但可以直接C,并通过cyclic -l来确定溢出值

可以大概猜出这里就是system

arm32位
这个arm32位的话,传前四个参数是用的r0~r3寄存器,如果参数再多的话,就利用栈传参(从右向左依次入栈)。函数的返回值会存在r0寄存器中。然后pc寄存器就相当于x86中的eip寄存器(始终装的都是我们下一条指令执行的地址)除此之外,arm 的 b/bl 等指令实现跳转。

直接用r0,pc,控制r0为binsh,pc为system最后getshell
1 | from pwn import* |
chall(ret2libc)
题目

前边一个base64换码表,把密文加密发送一下就能绕过

这里有栈溢出,但没有后门,很容易就想到打ret2libc,去找一下有没有我们需要的gadegt

没有我们想要的r0,r1,这里就需要其他的gadegt,在init中有一段gagdet可以控制程序流程,同时可以控制r1,r0并完成一次跳转,程序也不会退出,返回的main
1 | MOV R1, R8 |
这里泄露出libc之后就是再次利用这个gadget getsshell
1 | from pwn import* |
Shanghai2018 – baby_arm(ret2csu)
题目

可以注意到第一次read在bss段,后续有一个栈溢出,按照x86的思路会读入shellcode并栈溢出返回执行,题目还给出了protect函数,但我们需要控制mprotect的三个参数,分别是x0(buf),x1(length),x2(prot),这里就用到了ret2scu的思路
有个问题,栈溢出并不能直接控制返回地址,而是[sp]+0x10赋值给x29,x30,之后就能控制返回地址0x30,这里我们需要在构造payload的时候注意
1 | ldp x29, x30, [sp], #0x10 |

csu_gadget
1 | gagdet2 |
gadget1
LOP就是将sp+0x10,sp+0x18的数值赋值给x19,x20,后边类似,这样我们就可以控制x19,x21,x22,x23这些寄存器的值
gadget2
第一条指令的作用是从内存地址 x21 + (x19 << 3) 中加载一个64位的数据,并将其存入寄存器 x3 中。
x3需要是一个指针,指向我们想要返回地址地址,之后x22,x23分别对应x2,x1
详细可参考这位佬关于学习arm架构下的pwn的总结 - ZikH26 - 博客园
1 | from pwn import* |
总结
初探mips-arm-pwn,漏洞利用思路和x86架构下的基本相同,学习一种新的架构需要先补充一定的理论知识,包括汇编指令,函数调用机制,本地运行和调试。也学到了qemu的一些初步用法,接下来会深入研究异架构在堆上的利用+在实战中的应用,个人感觉ctf中的题目接近实际,但实际更复杂,但利用方法思路,都是一样的,更需要去审读代码,找到可以利用的gadget,完成利用
gdb调试之linux pc和linux arm环境下_can't find gdb-CSDN博客
基于实战的MIPS路由器栈溢出利用:寄存器、指令与攻击思路的探究-先知社区
深入异构 PWN:PowerPC&ARM&MIPS-先知社区
从一道mips题目学习搭建mips环境及ROP-腾讯云开发者社区-腾讯云
【Pwn 笔记】跨平台架构的环境配置与调试 | binLep's Blog