上周末抽空打了一下HarmonyOS和HMS专场CTF,做了两个Risc-V的Pwn题目。
计算机指令集可以分为两种:复杂指令集和精简指令集。
复杂指令集以x86指令集最为常见,多用于传统桌面软件,善于处理复杂的计算逻辑。精简指令集有ARM、MIPS和Risc-V等。ARM广泛应用于移动手持终端以及IoT设备,但是ARM指令集虽然开放但是授权架价格太高,而Risc-V是一套开源的精简指令集架构,企业可以完全免费的使用。
目前来讲,现有的工具链已经足以支持Risc-V的逆向分析。
在静态分析层面,Ghidra 9.2对于Risc-V的反编译效果不错(IDA 7.5尚不支持Risc-V),所以静态分析Risc-V用ghidra已经足够。
在动态调试层面,qemu已经集成了risc-v架构,可以支持该架构的模拟运行。在有lib的情况下,通过QEMU用户模式,添加-L
参数选择lib路径,通过-g
指定调试端口。在gdb高版本中,已经可以支持risv架构,同时配合gef插件,设置target remote连接QEMU的调试端口:
可以对risc-v进行正常的调试,包括下断点,查看内存等常见功能。
解决了工具链的问题,在逆向层面来讲,已经大大的降低了risc-v分析的门槛。为了更好的进行漏洞挖掘与利用,需要稍微学习一下risc-v的函数调用规则和指令集。
Risc-V函数调用也是基于寄存器和栈的,每次函数调用时优先利用寄存器传参,如果寄存器无法满足需求再利用栈传参。Risc-V寄存器种类和数量如下表所示:
在参数保存之后,通过jal
指令跳转到函数开始执行。jal指令的规范为:
jal ra, offset
将会把下一条指令(pc+4)地址存放到ra寄存器中,然后跳转到当前地址+offset位置开始执行。
在子函数中,将会把ra寄存器存放到栈上,在函数返回时从栈上恢复ra寄存器,这里也就存在栈溢出的机会
。
我们以下面这段代码作为demo:
#include <stdio.h>
int add1(int m ,int n)
{
return m + n ;
}
int add2(int m ,int n)
{
char ss[10] = "hello";
printf("%s", ss);
return m + n ;
}
int main(){
int m = 2;
int n = 3;
int sum1 = add1(m , n);
printf("%d\n", sum1);
int sum2 = add2(m , n);
printf("%d\n", sum2);
return 0;
}
编译后看一下反汇编:
将参数mv到a0和a1寄存器上后,跳转执行add1函数:
此时add1函数并没有调用子函数,即为叶子函数
,此时并不需要从栈中恢复ra寄存器。
而在add2中,在函数开始位置将ra寄存器存放到栈上:
在函数结束后从栈上恢复ra寄存器:
这是一个简单的shell,可以支持touch
、ls
、rm
等命令:
echo里面有一个栈溢出