Qiling框架学习-Qilinglab

Qiling框架学习-Qilinglab

简介

Qiling框架是一个超轻量级的“沙盒”,适用于Linux、MacOS、Windows、FreeBSD、DOS、UEFI和MBR。它是建立在Unicorn引擎之上的二进制仿真框架,支持x86(16、32和64位)、ARM、ARM64和MIPS。麒麟框架也凭借Demigod支持Linux内核模块(.ko),微软视窗驱动(.sys)和苹果MacOS内核(.kext)。

然而,麒麟框架不是旨于构建另一个“沙盒”工具,而是为逆向工程设计的框架。因此,二进制检测和API是麒麟框架的主要及优先关注点。使用麒麟框架可以节省时间。拥有丰富API的麒麟框架将逆向工程及二进制代码检测快速的提升到了一个新的层次。

此外,麒麟框架还提供了对寄存器、内存、文件系统、操作系统和调试器的API访问。麒麟框架也提供了虚拟机级别的API,如保存和恢复执行状态。

安装

使用pip安装

pip3 install qiling

使用 pip 安装最新的开发版本

pip3 install --user https://github.com/qilingframework/qiling/archive/dev.zip

从github克隆框架手动安装

git clone https://github.com/qilingframework/qiling

cd qiling

python3 setup.py install

另外不要忘记初始化 rootfs。

git submodule update --init --recursive

qilinglab

qilinglab是一个包含十几个小挑战的程序,用于快速上手Qiling框架的主要功能。
因为平时接触到的ARM架构相对来说比较多,我这里以arm架构作为学习的开始

首先使用file指令查看下载到的qilinglab程序
在这里插入图片描述

基本使用模板

from qiling import *

def challenge1(ql: Qiling):
    pass

if __name__  == '__main__':
    path = ['qilinglab-aarch64'] # 可执行程序
    rootfs = "/qiling/examples/rootfs/arm64_linux" # 机器文件系统的根
    ql.verbose = 0
    ql = Qiling(path, rootfs)
    ql.run()
#如果需要其他共享库来模拟二进制文件,我们需要下载它们并将它们添加到我们的 rootfs 中

在ql.run()前加一句ql.verbose = 0方便看输出内容
verbose = 0 为不在标准输出流输出日志信息 verbose = 1 为输出进度条记录

执行二进制程序
在这里插入图片描述

输出挑战列表,并提示Some challenges will results in segfaults and infinite loops if they aren’t solved

同步放在IDA Pro里面查看
因为本身lab是为了熟悉qiling框架,所以程序没有去除符号表和做混淆,很直观可以看到逆向后的程序逻辑

main()–>start()函数,主要是输出挑战内容,调用challange X函数和checker函数对结果进行校验
start()
在这里插入图片描述

checker()
在这里插入图片描述

challange1

题目要求:在0X1337地址处写入1337

在这里插入图片描述

操作内存手册:https://docs.qiling.io/en/latest/memory/
麒麟提供了几种管理模拟内存空间的方法:
在这里插入图片描述

写入内存地址
ql.mem.write(address, data)

映射内存区域
在写入内存之前映射内存。info可以为空。
ql.mem.map(addr,size,info = [my_first_map])

地址:
你需要对齐内存偏移量和地址以进行映射。
addr//size*size -> 0x7fefc9e0//4096*4096

大小:
应映射的内存量

此参数取决于操作系统;如果使用 linux 系统,请考虑至少使用 4096 的倍数进行对齐

slove-challenge1

def challenge1(ql):
    
    ql.mem.map(0x1337//4096*4096,0x1000, info = "[challenge1]")
    ql.mem.write(0x1337, ql.pack16(1337))
    # pack16(value) == struct.pack('H', value) 
    #struct.pack用于将Python的值根据格式符,转换为字符串,h表示short,l表示long

运行结束后可以通过kill命令手动结束程序运行
在这里插入图片描述

challange2

题目要求:使uname系统调用返回正确的值

在这里插入图片描述

uname系统调用返回有关底层操作系统的信息,传入一个utsname结构体buffer让它填充

utsname结构体的相关信息参考
https://man7.org/linux/man-pages/man3/uname.3p.html
在这里插入图片描述

struct utsname
{
    char sysname[65];
    char nodename[65];
    char release[65];
    char version[65];
    char machine[65];
    char domainname[65];
};

通过挑战条件为

uname.sysname == "QilingOS";
uname.version == "ChallengeStart";

需要通过qiling提供的系统调用uname,修改返回地uname结构体即可。
https://docs.qiling.io/en/latest/hijack/
在这里插入图片描述

劫持 POSIX 系统调用

POSIX 系统调用可以hook以允许用户修改其参数、更改返回值或完全替换其功能。当指定的系统调用即将被调用时,系统调用可以通过其名称或号码挂钩,并在一个或多个阶段被拦截;
进入系统调用前,可用于完全替换系统调用功能;
退出系统调用后,可用于篡改系统调用参数值,可用于篡改返回值;
QL_INTERCEPT.CALL| QL_INTERCEPT.ENTER | QL_INTERCEPT.EXIT

slove-challenge2
在这里插入图片描述

JOANSIVION大佬这里的方法是获取寄存器sp栈指针的地址,然后找到偏移量,就是结构体的指针地址,然后作为mem写入的起始地址
name的地址为 -0x1B0
然后0x1F0+name = 0x1F0-0x1B0 = 0x40

def hook_uname_on_exit(ql, pName, *args):
    #out_struct_addr = ql.arch.regs.sp + 0x40
    #sysname_addr = out_struct_addr
    #ql.mem.write(sysname_addr, b'QilingOS\x00')
    #ql.mem.write(out_struct_addr + 65 * 3, b'ChallengeStart\x00')
    #使用这种方法定义函数时为def hook_uname_on_exit(ql, *args):
    ql.mem.write(pName, b'QilingOS\x00')
    ql.mem.write(pName + 65 * 3, b'ChallengeStart\x00')
    #通过 os.set_syscall 加上 QL_INTERCEPT.EXIT 参数,在调用结束后劫持 uname 的返回值,替换成验证的字符串
    
def challenge2(ql):
    ql.os.set_syscall('uname', hook_uname_on_exit, QL_INTERCEPT.EXIT)
    #系统调用可以通过其名称或号码来引用,通过引用其编号替换uname系统调用的等效替代

在这里插入图片描述

challange3

题目要求:/dev/urandom 和 getrandom 相等

在这里插入图片描述

程序需要使/dev/urandom 和 getrandom 相等,且一个字节的随机数和其他的随机数都不一样。

getrandom的系统调用定义如下

ssize_t getrandom(void *buf, size_t buflen, unsigned int flags);
/*
The getrandom() system call fills the buffer pointed to by buf
with up to buflen random bytes.  These bytes can be used to seed
user-space random number generators or for cryptographic purposes.
*/

对 getrandom的劫持与Challenge2一样

对 /dev/urandom的劫持要用到 QlFsMappedObject的add_fs_mapper,它可以实现将模拟环境中的路径劫持到主机上的路径或将读/写操作重定向到用户定义的对象。

在这里插入图片描述

slove-challenge3

class Fake_urandom(QlFsMappedObject):

    def read(self, size):
        # return a constant value upon reading
        if(size == 1):
            return b"\x02"  # byUrandom
        else:
            return b"\x01" * size

    def fstat(self): # syscall fstat will ignore it if return -1
        return -1

    def close(self):
        return 0
    
def fake_getrandom(ql, buf, buflen, flags, *args, **kw):
    ql.mem.write(buf, b"\x01"*buflen)
    ql.os.set_syscall_return(0)
    
def challenge3(ql):
    ql.add_fs_mapper("/dev/urandom", Fake_urandom())
    #将虚拟路径映射到用户定义的文件类型,该对象允许对交互进行更精细的控制
    ql.os.set_syscall('getrandom', fake_getrandom, QL_INTERCEPT.EXIT)
    #同2系统调用

在这里插入图片描述

challenge 4

题目要求:进入禁止的循环
IDA F5无效
在这里插入图片描述

查看汇编
在这里插入图片描述

loc_FD8()函数
LDR 将存储器地址为SP+0x20-8的半字数据读入寄存器w0
LDR 将存储器地址为SP+0x20-4的半字数据读入寄存器w1
CMP 比较W1和W0的值
B.LT 比较结果是大于,跳转loc_FC0()函数,否则不跳转
因为判断条件一直不成立,程序进入死循环,不会跳转循环语句
我们需要在比较前将W0值改为1
我们需要使用ql
https://docs.qiling.io/en/latest/hook/

slove-challenge4

def forbidden_loop_hook(ql):

    #hook_address 将 x0 改成比 x1 小即可
    #ql.arch.regs.x0 = 1
    #ql.arch.regs.write("x0", 1)

    ql.arch.regs.write("w0", 0x1)
 
def challenge4(ql):

    # Get the module base address

    # https://github.com/qilingframework/qiling/blob/dev/qiling/profiles/linux.ql 可知 qiling 默认配置 linux64 加载基地址为 0x555555554000

    #base_addr = ql.mem.get_lib_base(ql.path)

    # Address we need to patch
    test_forbidden_loop_enter = 0x555555554000 + 0xFE0

    # cmp指令偏移是 0xFE0
    # Place hook
    ql.hook_address(forbidden_loop_hook, test_forbidden_loop_enter)

cmp指令偏移是 0xFE0
在这里插入图片描述

在这里插入图片描述

challenge5

题目要求:预测每次调用rand()函数的值

在这里插入图片描述

在第二个for循环中,存在判断语句rand()函数的值是否都相同为0,我们需要挟持rand()函数的值让它都是0

rand()是库函数,不是系统调用,所以不能用set_syscall,应该用set_api

参考Qiling文档Hijacking OS API (POSIX)
在这里插入图片描述

slove-challenge5

def rand_hook(ql, *args, **kw):

    ql.arch.regs.x0 = 0

def challenge5(ql):

    ql.os.set_api("rand", rand_hook)

在这里插入图片描述

这里为什么没有报sloved,排查问题也排查不出呀

challenge6

题目要求:避免无限循环

在这里插入图片描述

在这里插入图片描述

B.NE 表示不相等时直接向后跳转
程序不断的将1 mov至寄存器w0,然后cmp w0 和0的值,不相同时跳转,程序陷入无限死循环,我们需要在cmp前使w0=0.就可以跳出循环

slove-challenge6

def infinite_loop_bypass_hook(ql):

    ql.arch.regs.write("w0", 0x0)

def challenge6(ql):

    # Address we need to patch

    # cmp_infinite_loop_addr = base_addr + 0x1118 = 0x555555554000 + 0x1118

    # Place hook

    ql.hook_address(infinite_loop_bypass_hook, 0x555555554000 + 0x1118)

在这里插入图片描述

输出 challenge5 SOLVED
之前卡住判断是因为challnege6存在死循环,然后解决完死循环的问题,challenge5就解决了

challenge 7

题目要求:不要浪费时间等待sleep()函数

在这里插入图片描述

这里可以挟持sleep()函数,修改sleep的值

slove-challenge7

def fake_sleep(ql, *args):
    ql.arch.regs.write("w0", 0)
    #法二 
    #return
def hook_nanosleep(ql: Qiling, *args, **kwargs):
    # 注意参数列表
    return

def challenge7(ql):
    ql.os.set_api("sleep", fake_sleep)
    #法三
    #ql.set_syscall('nanosleep', hook_nanosleep)

这个challenge7解决后,程序恢复正常,开始输出各个challenge的信息

参考其他大佬的方法,这里还有几种解决方法:
1、挟持sleep()函数,将其替换为空函数
2、劫持系统调用,根据DEBUG信息,sleep其实是调用了nanosleep()

参考
https://man7.org/linux/man-pages/man3/sleep.3.html
https://man7.org/linux/man-pages/man2/nanosleep.2.html
	On Linux, sleep() is implemented via nanosleep(2). 

在这里插入图片描述

challenge8

题目要求:解包结构体并在目标地址写入内容

在这里插入图片描述

在这里插入图片描述

NOP 无操作

这个题目没咋看懂,函数调用了两次malloc()函数,结构体嵌套?
看了一下其他人的方法,是通过利用特殊字符串或者结构定位想要的指令地址
通过固定的 0x3DFCD6EA539 去找到结构体位置,进而修改 flag

qiling从内存搜索字符串
在这里插入图片描述

这里才理解了QQQ的原理

slove-challenge7

def search_heap1(ql):
#从内存中搜索字符串
    nMagic = 0x3DFCD6EA00000539
    pMagics = ql.mem.search(ql.pack64(nMagic))
#内存可能出现了几次字符串,使用字符串“Random data”验证是否找到了正确的数据
    for pMagic in pMagics:
    	pHeap1 = pMagic - 8    	
    	heap1 = ql.mem.read(pHeap1, 24)
    	pHeap2, _, pFlag = struct.unpack("QQQ", heap1)  	
#比较地址和读到的字符串
    	if ql.mem.string(pHeap2) == "Random data":
#找到结构体的位置然后写入1
    		ql.mem.write(pFlag, b"\x01")
    		break

def challenge8(ql):

    ql.hook_address(search_heap1, 0x555555554000 + 0x11DC) # 0x11DC : nop

在这里插入图片描述

这个题目确实有点云里雾里,后续再研究研究

challenge9

题目要求:修改字符串操作使得 iMpOsSiBlE 正确

在这里插入图片描述

tolower(int c) 把给定的字母转换为小写字母

解决的两个思路
1、在两个字符串比较前,让tolower()失效
2、直接修改strcmp(src, dest) == 0 的值

solve-challenge9

def fake_strcmp(ql, *args):

    ql.arch.regs.write("x0", 0)  

def fake_tolower(ql):
    return

def challenge9(ql):

    #ql.os.set_api('strcmp', fake_strcmp)

    ql.os.set_api('tolower', fake_tolower)

在这里插入图片描述

方法二同时会解决challenge2,后面看一下

challenge10

题目要求:伪造成 ’cmdline’ 文件来返回正确的内容
在这里插入图片描述

通过篡改/proc/self/cmdline 这个文件内容为”qilinglab”

解决的两个思路
1、像challenge3对 /dev/urandom的劫持的那样,通过 add_fs_mapper 映射到自定义实现或主机路径,它可以实现将模拟环境中的路径劫持到主机上的路径或将读/写操作重定向到用户定义的对象。
2、直接修改strcmp(buf,“qilinglab”) == 0 的值,这也是为啥challenge 9的方法二可以同时解决challenge 10

solve-challenge10

#未通过
class Fake_cmdline(QlFsMappedObject):
    def read(self, size):
        return b'qilinglab'

    def fstat(self):
        return -1

    def close(self):
        return 0

def challenge10(ql):

    ql.add_fs_mapper("/proc/self/cmdline", Fake_cmdline())

在这里插入图片描述

JOANSIVION也提到直接用我们的另一个主机文件系统替换目标文件
本地创建一个文本

echo -n "qilinglab" > fake_cmdline
ql.add_fs_mapper("/proc/self/cmdline", "./fake_cmdline")

为啥大家这样解决都没问题,我challenge9无论采用ql.os.set_api(‘tolower’, fake_tolower)的方案,还是直接替换目标文件,后面这里一直通不过呀???
有无大佬这里能指点一下
最后只能采用挟持strcmp()函数的方案

challenge11

题目要求:绕过CPUID/MIDR_EL1检查

在这里插入图片描述

在这里插入图片描述

MRS CPUid指令,可以加载特殊功能寄存器的值到通用寄存器
aarch64的伪代码:

if ( _ReadStatusReg(ARM64_SYSREG(3, 0, 0, 0, 0)) >> 16 == 4919 )
  {
    result = (__int64)a1;
    *a1 = 1;
  }

这里是将CPU的信息保存到X0中,然后运行比较运算

solve-challenge11
为了通过挑战,我们需要将返回值替换为0x1337

简便方法:

def fake_end(ql):
    ql.arch.regs.write("x1", 0x1337)

def challenge11(ql):
    ql.hook_address(fake_end, 0x555555554000+ 0x1400)

或者采用qiling的函数 hook_code(),可以hook CPU所有的指令,

在这里插入图片描述

法二:

def midr_el1_hook(ql, address, size):
    # opcode: \x00\x00\x38\xD5  
    if ql.mem.read(address, size) == b"\x00\x00\x38\xD5":
        # Write the expected value to x0
        ql.arch.regs.x0 = 0x1337 << 16
        # Go to next instruction
        # opcode take 4 bytes so next instruction will be pc + 4
        ql.arch.regs.arch_pc += 4

def challenge11(ql):

    ql.hook_code(midr_el1_hook)

在这里插入图片描述

完整代码

import struct

from qiling import *
# from unicorn.unicorn_const import UC_MEM_WRITE
from qiling.const import *
from qiling.os.mapper import QlFsMappedObject

def challenge1(ql):
    #ql.mem.map(addr, size) must be page aligned
    ql.mem.map(0x1000, 0x1000, info = "[challenge1]")
    ql.mem.write(0x1337, ql.pack16(1337))

def hook_uname_on_exit(ql, *args):
    #sp = ql.arch.regs.sp
    out_struct_addr = ql.arch.regs.sp + 0x40
    sysname_addr = out_struct_addr
    ql.mem.write(sysname_addr, b'QilingOS\x00')
    ql.mem.write(out_struct_addr + 65 * 3, b'ChallengeStart\x00')
    #ql.mem.write(pName, b'QilingOS\x00')
    #ql.mem.write(pName + 65 * 3, b'ChallengeStart\x00')

def challenge2(ql):
    ql.os.set_syscall('uname', hook_uname_on_exit, QL_INTERCEPT.EXIT)

    #系统调用可以通过其名称或号码来引用,通过引用其编号替换uname系统调用的等效替代

class Fake_urandom(QlFsMappedObject):
    def read(self, size):

        if(size == 1):
            return b"\x02"  # byUrandom

        else:
            return b"\x01" * size
    def fstat(self): # syscall fstat will ignore it if return -1
        return -1
    def close(self):
        return 0

def fake_getrandom(ql, buf, buflen, flags, *args, **kw):

    ql.mem.write(buf, b"\x01"*buflen)
    ql.os.set_syscall_return(0)

def challenge3(ql):

    ql.add_fs_mapper("/dev/urandom", Fake_urandom())
    ql.os.set_syscall('getrandom', fake_getrandom, QL_INTERCEPT.EXIT)

def forbidden_loop_hook(ql):

    #ql.arch.regs.x0 = 1
    #hook_address 将 x0 改成比 x1 小即可
    #ql.arch.regs.write("x0", 1)
    ql.arch.regs.write("w0", 0x1)

def challenge4(ql):

    # Get the module base address
    # https://github.com/qilingframework/qiling/blob/dev/qiling/profiles/linux.ql 可知 qiling 默认配置 linux64 加载基地址为 0x555555554000
    #base_addr = ql.mem.get_lib_base(ql.path)
    # Address we need to patch
    test_forbidden_loop_enter = 0x555555554000 + 0xFE0
    # cmp指令偏移是 0xFE0
    # Place hook

    ql.hook_address(forbidden_loop_hook, test_forbidden_loop_enter)

def rand_hook(ql, *args, **kw):
    ql.arch.regs.x0 = 0

def challenge5(ql):
    ql.os.set_api("rand", rand_hook)

def infinite_loop_bypass_hook(ql):
    ql.arch.regs.write("w0", 0x0)

def challenge6(ql):

    # Get the module base address
    #base_addr = ql.mem.get_lib_base(ql.path)
    #print(base_addr)
    # Address we need to patch
    # cmp_infinite_loop_addr = base_addr + 0x1118
    # Place hook

    ql.hook_address(infinite_loop_bypass_hook, 0x555555554000 + 0x1118)

def fake_sleep(ql, *args):
    ql.arch.regs.write("w0", 0)

def challenge7(ql):
    ql.os.set_api("sleep", fake_sleep)

def search_heap1(ql):
    nMagic = 0x3DFCD6EA00000539;
    pMagics = ql.mem.search(ql.pack64(nMagic))
    for pMagic in pMagics:
    	pHeap1 = pMagic - 8
    	heap1 = ql.mem.read(pHeap1, 24)
    	pHeap2, _, pFlag = struct.unpack("QQQ", heap1)

    	if ql.mem.string(pHeap2) == "Random data":
    		ql.mem.write(pFlag, b"\x01")
    		break
    		return

def challenge8(ql):
    #pBase = ql.mem.get_lib_base(ql.path)
    ql.hook_address(search_heap1, 0x555555554000 + 0x11DC) # 0x11DC : nop
#def fake_strcmp(ql, *args):
    #ql.arch.regs.write("x0", 0)
def fake_tolower(ql):
    return

def challenge9(ql):
    #ql.os.set_api('strcmp', fake_strcmp)
    ql.os.set_api("tolower", fake_tolower)   

class Fake_cmdline(QlFsMappedObject):
    def read(self, size):
        return b"qilinglab"
    def close(self):
        return 0

def challenge10(ql):
    ql.add_fs_mapper("/proc/self/cmdline", Fake_cmdline())

def fake_end(ql):
    ql.arch.regs.write("x1", 0x1337)

def challenge11(ql):
    ql.hook_address(fake_end, 0x555555554000+ 0x1400)   

if __name__ == '__main__':
    path = ["./qilinglab-aarch64"]
    rootfs = "/home/snjuxp/qiling/examples/rootfs/arm64_linux"
    ql = Qiling(path, rootfs, verbose=QL_VERBOSE.DEBUG)
    #ql = Qiling(path, rootfs)
    ql.verbose = 0
    #ql.verbose = 4
    # ql.mem.map_info()
    #ql.mem.get_formatted_mapinfo()
    challenge1(ql)
    challenge2(ql)
    challenge3(ql)
    challenge4(ql)
    challenge5(ql)
    challenge6(ql)
    challenge7(ql)
    challenge8(ql)
    challenge9(ql)
    challenge10(ql)
    challenge11(ql)
    ql.run()

总结

这应该是农历新年的最后一篇文章了,同时也是2023新年的第一篇文章。今年学习了解了不少知识,射频安全,BLE,Fuzz,还有一些安全方案,2023年还有很多方向需要深入研究的,英语也需要多多练习口语,车联网安全的学习还任重而道远。2022的前半年都在疫情中度过,年末又恶感新冠,无论如何,希望2023身体健康。

参考:
https://docs.qiling.io/
https://bbs.kanxue.com/thread-268989.htm#msg_header_h2_13
https://joansivion.github.io/qilinglabs/
https://ryze-t.com/2022/09/08/Qiling%E6%A1%86%E6%9E%B6%E5%85%A5%E9%97%A8-QilingLab/
https://blog.csdn.net/Ga4ra/article/details/124412806
https://github.com/badmonkey7/qilinglab-solution

  • 2
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值