Qiling入门与QilingLab
2023-06-04 19:44:05

Qiling入门与QilingLab

Qiling

二进制仿真框架,支持多种文件格式(PE,MachO,ELF,COM)

项目地址:

https://github.com/qilingframework/qiling

官方文档:

https://docs.qiling.io/en/latest/

安装使用(官方文档中也有):

#此处采用Ubuntu20.04
sudo apt-get update
sudo apt-get upgrade
sudo apt install python3-pip git cmake

git clone https://github.com/qilingframework/qiling
cd qiling
sudo pip3 install . 

git submodule update --init --recursive

QilingLab

一个关于Qiling使用的挑战学习,旨在学习Qiling的使用,所以对于挑战将采用预期解

https://www.shielder.com/blog/2021/07/qilinglab-release/

挑战分为两个架构:x86_64与aarch64,下文中是对于x86_64的挑战writeup:

下载文件 –> 64位IDA pro打开进行静态分析:

一共是11个挑战,且没有去除符号表

尝试执行程序:

使用Qiling模拟启动程序:

from qiling import *
from qiling.const import QL_INTERCEPT
from qiling.os.mapper import QlFsMappedObject
import os
import struct

if __name__  == '__main__':
    path = ['qilinglab-x86_64'] #目标文件
    rootfs = "/home/iotserver/tools/qiling/examples/rootfs/x8664_linux" # 文件系统 在clone下来的仓库里
    ql = Qiling(path, rootfs)
    
    ql.verbose = 0  #查看输出内容 从1~n
    ql.run()

依旧是报错(放心食用)

ql.verbose = 0  #从下图中可以看到由此输出了寄存器等信息

challenge1

我们挑战成果的条件是挑战关卡函数传入的指针指向的位置能被赋值为1,如下图

此处的逻辑为判断内存中的0x1337中的值是否为1337,是的话挑战成功,为此,使用Qiling直接修改内存即可;

但是由于我们不能确定程序的加载基地址,所以需要映射一块内存使用,并且由于Qiling底层使用Unicorn Engine,所以在进行内存映射时要采用4k对齐

文档相关位置:

内存操作相关:

https://docs.qiling.io/en/latest/memory/#mapping-memory-pages

打包与解包:

https://docs.qiling.io/en/latest/struct/

使用到的内容

#映射内存页
ql.mem.map(addr: int, size: int, perms: int = UC_PROT_ALL, info: Optional[str] = None) -> None
#addr:映射基地址
#size:以字节为单位的映射大小
#info:为映射范围设置字符串标签,以便于识别
#例:
ql.mem.map(0x1000, 0x1000, info='[challenge1]')

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

#打包16位数据,无符号整型,2字节
ql.pack16()

==》

from qiling import *
from qiling.const import QL_INTERCEPT
from qiling.os.mapper import QlFsMappedObject
import os
import struct

def challenge1(ql: Qiling):
    ql.mem.map(0x1000, 0x1000, info='[challenge1]')   #映射内存
    ql.mem.write(0x1337, ql.pack16(1337))   #修改内存的值

if __name__  == '__main__':
    path = ['qilinglab-x86_64'] #目标文件
    rootfs = "/home/iotserver/tools/qiling/examples/rootfs/x8664_linux" # 文件系统 在clone下来的仓库里
    ql = Qiling(path, rootfs)
    
    # 在ql.run()之前,做好我们的hook工作
    challenge1(ql) 
    
    ql.verbose = 0  #查看输出内容 从1~n
    ql.run()

第一关通过

challenge2

函数通过系统调用uname获取系统信息,并对比,校验成功则通过

//<sys/utsname.h>
struct utsname
{
    char sysname[65];
    char nodename[65];
    char release[65];
    char version[65];
    char machine[65];
    char domainname[65];
};

为此,我们将在系统调用返回时对其进行Hook,达到我们想要的功能

image-20230604172520573

文档相关位置:

劫持:

https://docs.qiling.io/en/latest/hijack/

Hook:

https://docs.qiling.io/en/latest/hook/

寄存器操作:

https://docs.qiling.io/en/latest/register/

==》

from qiling import *
from qiling.const import QL_INTERCEPT
from qiling.os.mapper import QlFsMappedObject
import os
import struct

def challenge1(ql: Qiling):
    ql.mem.map(0x1000, 0x1000, info='[challenge1]')   #映射内存
    ql.mem.write(0x1337, ql.pack16(1337))   #修改内存的值

def hook_uname_exit(ql: Qiling, *args):  #hook uname系统调用返回的内容
    rdi = ql.arch.regs.rdi   #获取寄存器
    ql.mem.write(rdi, b'QilingOS\x00')  #写入
    ql.mem.write(rdi + 65 * 3, b'ChallengeStart\x00')  #根据utsname结构体获取要写入的位置,长度不对的话会报错
    
def challenge2(ql: Qiling):
    #hook系统调用返回
    #QL_INTERCEPT.EXIT  在退出系统调用后
    ql.os.set_syscall('uname', hook_uname_exit, QL_INTERCEPT.EXIT)   
    
if __name__  == '__main__':
    path = ['qilinglab-x86_64'] #目标文件
    rootfs = "/home/iotserver/tools/qiling/examples/rootfs/x8664_linux" # 文件系统 在clone下来的仓库里
    ql = Qiling(path, rootfs)
    
    # 在ql.run()之前,做好我们的hook工作
    challenge1(ql) 
    challenge2(ql)
    
    ql.verbose = 0  #查看输出内容 从1~n
    ql.run()

成功通关

image-20230604173953952

challenge3

image-20230604174423099

函数将先读取/dev/urandom获取随机数,再通过getrandom()获取随机数,后续需要比对这两个随机数;程序还读取了一个一字节的随机数,并且要求与第一个读取的随机数不一样

文档相关位置:

https://docs.qiling.io/en/latest/hijack/

Qiling提供了QlFsMappedObject去自定义文件系统,例:read,write等

由于全粘贴在一起代码太长了,所以后面都直接粘贴实现的函数了

class Fake_urandom(QlFsMappedObject):
    def read(self, size):
        if size == 1:   #读取1字节,随便自定义个数字
            return b"\x01"
        return b"\x00"*size  #该处读取要与getrandom()获取的随机数进行比对
    def close(self):
        return 0

#对getrandom()进行hook
def getrandom_hook(ql, buf, buflen, flags, *args, **kw):
    ql.mem.write(buf, b"\x00"*buflen)
    ql.os.set_syscall_return(0)  #设置返回  可设置可不设?

def challenge3(ql):
    ql.os.set_syscall("getrandom", getrandom_hook)
    ql.add_fs_mapper("/dev/urandom", Fake_urandom())

成功通过

challenge4

IDA pro:

image-20230604181354933

其实就是一个循环,这里我用Ghidra反编译试了一下:

image-20230604181432643

所以思路页变得非常简单,进入一次循环即可,也就是将条件设置为1即可,就变成

for(int i=0; i < 1; i++){ *a1 = 1; }

这里我们将直接采用hook地址的方式==》

def hook_eax(ql: Qiling):
    ql.arch.regs.eax = 1

def challenge4(ql: Qiling):
    '''
    0000000000000E40 8B 45 F8            mov     eax, [rbp+var_8]
    0000000000000E43 39 45 FC            cmp     [rbp+var_4], eax
    0000000000000E46 7C ED               jl      short loc_E35
	'''
    libc_base = ql.mem.get_lib_base(ql.path)#获取基地址
    hook_addr = libc_base + 0xE43
    ql.hook_address(hook_eax,hook_addr)

成功通过

challenge5

由于此题就算你感觉你做出来了,但是由于后续题目的影响,是不会有回显的,可能会让你不放心,这边建议可以做完challenge6在来看就有回显了

image-20230604182402305

这个就比较简单了,程序从rand()获取随机数,与0对比,只要让rand()每次返回都是0就可以了

def hook_rand(ql: Qiling):
    ql.arch.regs.rax = 0  #设置寄存器

def challenge5(ql: Qiling):
    ql.os.set_api('rand', hook_rand) #hook外部函数

通过但无回显,正常现象

image-20230604182920488

challenge6

image-20230604182958390

image-20230604183115550

是一个死循环,直接在进行比较之前修改al寄存器的值即可

def hook_while_true(ql: Qiling):
    ql.arch.regs.rax = 0  #设置寄存器

def challenge6(ql: Qiling):
    base = ql.mem.get_lib_base(ql.path)  #获取基地址
    ql.hook_address(hook_while_true, base + 0xF16)  #hook

通过但无回显,但是回显了第五关

image-20230604183331314

challenge7

image-20230604183425532

直接就是一个睡,这位更是重量级

可以通过修改sleep()的参数;自己实现sleep() api;修改sleep()的底层系统调用nanosleep()。

def set_sleep_edi(ql: Qiling):
    ql.arch.regs.edi = 0 #修改参数

def hook_sleep_api(ql: Qiling):
    return  #直接就是一个返回

def hook_nanosleep(ql: Qiling, *args, **kwargs):
    # 注意参数列表
    return

def challenge7(ql: Qiling):
    #QL_INTERCEPT.ENTER  在进入系统调用之前
    # ql.set_api('sleep', set_sleep_edi , QL_INTERCEPT.ENTER)
    # ql.set_api('sleep', hook_sleep_api)
    ql.os.set_syscall('nanosleep', hook_nanosleep)

成功通过,至此,已经可以全部回显了

challenge8

这东西是个结构体

struct xxx{
    char *v2;
    __int64 magic;
    char *check;
}

文档相关位置:

结构体解包:

https://docs.qiling.io/en/latest/struct/

字节顺序,大小和对齐方式:

https://docs.python.org/3/library/struct.html#module-struct
https://docs.python.org/3/library/struct.html#format-characters
https://docs.python.org/3/library/struct.html#byte-order-size-and-alignment
def search_mem_to_find_struct(ql: Qiling):
    MAGIC = ql.pack64(0x3DFCD6EA00000539)
    candidate_addrs = ql.mem.search(MAGIC)  #内存中检索

    for addr in candidate_addrs:
        # 有可能有多个地址,所以通过其他特征进一步确认
        heap_addr = addr - 8
        heap = ql.mem.read(heap_addr, 24)  #读取结构体内容
        string_addr, _, check_addr = struct.unpack('QQQ', heap)  #解包数据。解成三个8字节的无符号长整型的数据
        if ql.mem.string(string_addr) == 'Random data':  #程序中还有一个字符串标志用以辨别
            ql.mem.write(check_addr, b'\x01')
            break

def challenge8(ql: Qiling):
    base = ql.mem.get_lib_base(ql.path)  #获取基地址
    ql.hook_address(search_mem_to_find_struct, base + 0xFB5)

成功通过:

challenge9

image-20230604191019889

这个也比较简单了,一个字符串,经过tolower()处理在与原字符串对比,直接改tolower()就可以了

def fake_tolower(ql: Qiling):
    return

def challenge9(ql: Qiling):
    ql.os.set_api('tolower', fake_tolower)

成功通过

challenge10

这一关我并没有按照预期解解出来,不知道哪里的问题,若是有师傅知道,请务必浇浇Orz

image-20230604191438864

逻辑上就是读取文件内容,然后判断,按照题目描述,这题的预期解应该是劫持文件系统

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

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

但是我并没有成功,百思不得其解,就直接先把strcmp()进行了hook,期待成功劫持文件系统的师傅浇浇

image-20230604191958252

def hook_strcmprax(ql:Qiling):
    ql.arch.regs.rax = 0

def challenge10(ql:Qiling):
    base = ql.mem.get_lib_base(ql.path)
    ql.hook_address(hook_strcmprax, base + 0x1132)

暂且通过

challenge11

最后一题

可以看出来重点在cpuid这个指令

image-20230604192353145

可以看到程序对esi,ecx,eax寄存器进行填充并对比,为此,我们直接设置这几个寄存器,对cpuid这个指令进行hook

def hook_cpuid(ql: Qiling, address, size):
    """
    000000000000118F 0F A2     cpuid
    """
    if ql.mem.read(address, size) == b'\x0F\xA2':
        ql.arch.regs.ebx = 0x696C6951
        ql.arch.regs.ecx = 0x614C676E
        ql.arch.regs.edx = 0x20202062
        ql.arch.regs.rip += 2

def challenge11(ql: Qiling):
    ql.hook_code(hook_cpuid)

成功通过

我还看到一个可以提高性能与准确性的写法

def hook_cpuid(ql: Qiling, address, size):
    """
    0000564846E0118F 0F A2      cpuid
    """
    if ql.mem.read(address, size) == b'\x0F\xA2':
        ql.arch.regs.ebx = 0x696C6951
        ql.arch.regs.ecx = 0x614C676E
        ql.arch.regs.edx = 0x20202062
        ql.arch.regs.rip += 2

def challenge11(ql: Qiling):
    begin, end = 0, 0
    for info in ql.mem.map_info:
        #5表示r-x属性
        #该判断缩小hook范围
        if info[2] == 5 and info[3] == 'qilinglab-x86_64':
            begin, end = info[:2]

    ql.hook_code(hook_cpuid, begin=begin, end=end)

至此,已经全部通过(除了第10关呜呜呜)

Writeup

from qiling import *
from qiling.const import QL_INTERCEPT
from qiling.os.mapper import QlFsMappedObject
import os
import struct

def challenge1(ql: Qiling):
    ql.mem.map(0x1000, 0x1000, info='[challenge1]')   #映射内存
    ql.mem.write(0x1337, ql.pack16(1337))   #修改内存的值

def hook_uname_exit(ql: Qiling, *args):  #hook uname系统调用返回的内容
    rdi = ql.arch.regs.rdi   #获取寄存器
    ql.mem.write(rdi, b'QilingOS\x00')  #写入
    ql.mem.write(rdi + 65 * 3, b'ChallengeStart\x00')  #根据utsname结构体获取要写入的位置,长度不对的话会报错
    
def challenge2(ql: Qiling):
    #hook系统调用返回
    #QL_INTERCEPT.EXIT  劫持OS API
    ql.os.set_syscall('uname', hook_uname_exit, QL_INTERCEPT.EXIT)   

class Fake_urandom(QlFsMappedObject):
    def read(self, size):
        if size == 1:   #读取1字节,随便自定义个数字
            return b"\x01"
        return b"\x00"*size  #该处读取要与getrandom()获取的随机数进行比对
    def close(self):
        return 0

#对getrandom()进行hook
def getrandom_hook(ql, buf, buflen, flags, *args, **kw):
    ql.mem.write(buf, b"\x00"*buflen)
    #ql.os.set_syscall_return(0)

def challenge3(ql):
    ql.os.set_syscall("getrandom", getrandom_hook)
    ql.add_fs_mapper("/dev/urandom", Fake_urandom())


def hook_eax(ql: Qiling):
    ql.arch.regs.eax = 1

def challenge4(ql: Qiling):
    '''
    0000000000000E40 8B 45 F8            mov     eax, [rbp+var_8]
    0000000000000E43 39 45 FC            cmp     [rbp+var_4], eax
    0000000000000E46 7C ED               jl      short loc_E35
	'''
    libc_base = ql.mem.get_lib_base(ql.path)#获取基地址
    hook_addr = libc_base + 0xE43
    ql.hook_address(hook_eax,hook_addr)

def hook_rand(ql: Qiling):
    ql.arch.regs.rax = 0  #设置寄存器

def challenge5(ql: Qiling):
    ql.os.set_api('rand', hook_rand) #hook外部函数
    

def hook_while_true(ql: Qiling):
    ql.arch.regs.rax = 0  #设置寄存器

def challenge6(ql: Qiling):
    base = ql.mem.get_lib_base(ql.path)  #获取基地址
    ql.hook_address(hook_while_true, base + 0xF16)  #hook

def set_sleep_edi(ql: Qiling):
    ql.arch.regs.edi = 0 #修改参数

def hook_sleep_api(ql: Qiling):
    return  #直接就是一个返回

def hook_nanosleep(ql: Qiling, *args, **kwargs):
    # 注意参数列表
    return

def challenge7(ql: Qiling):
    #QL_INTERCEPT.ENTER  在进入系统调用之前
    # ql.set_api('sleep', set_sleep_edi , QL_INTERCEPT.ENTER)
    # ql.set_api('sleep', hook_sleep_api)
    ql.os.set_syscall('nanosleep', hook_nanosleep)

def search_mem_to_find_struct(ql: Qiling):
    MAGIC = ql.pack64(0x3DFCD6EA00000539)
    candidate_addrs = ql.mem.search(MAGIC)  #内存中检索

    for addr in candidate_addrs:
        # 有可能有多个地址,所以通过其他特征进一步确认
        heap_addr = addr - 8
        heap = ql.mem.read(heap_addr, 24)  #读取结构体内容
        string_addr, _, check_addr = struct.unpack('QQQ', heap)  #解包数据。解成三个8字节的无符号长整型的数据
        if ql.mem.string(string_addr) == 'Random data':  #程序中还有一个字符串标志用以辨别
            ql.mem.write(check_addr, b'\x01')
            break

def challenge8(ql: Qiling):
    base = ql.mem.get_lib_base(ql.path)  #获取基地址
    ql.hook_address(search_mem_to_find_struct, base + 0xFB5)

def fake_tolower(ql: Qiling):
    return

def challenge9(ql: Qiling):
    ql.os.set_api('tolower', fake_tolower)

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

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

def hook_strcmprax(ql:Qiling):
    ql.arch.regs.rax = 0

def challenge10(ql:Qiling):
    base = ql.mem.get_lib_base(ql.path)
    ql.hook_address(hook_strcmprax, base + 0x1132)

def hook_cpuid(ql: Qiling, address, size):
    """
    000000000000118F 0F A2     cpuid
    """
    if ql.mem.read(address, size) == b'\x0F\xA2':
        ql.arch.regs.ebx = 0x696C6951
        ql.arch.regs.ecx = 0x614C676E
        ql.arch.regs.edx = 0x20202062
        ql.arch.regs.rip += 2

def challenge11(ql: Qiling):
    ql.hook_code(hook_cpuid)

if __name__  == '__main__':
    path = ['qilinglab-x86_64'] #目标文件
    rootfs = "/home/iotserver/tools/qiling/examples/rootfs/x8664_linux" # 文件系统 在clone下来的仓库里
    ql = Qiling(path, rootfs)
    
    # 在ql.run()之前,做好我们的hook工作
    challenge1(ql) 
    challenge2(ql)
    challenge3(ql)
    challenge4(ql)
    challenge5(ql)
    challenge6(ql)
    challenge7(ql)
    challenge8(ql)
    challenge9(ql)
    challenge10(ql)
    challenge11(ql)
    
    ql.verbose = 0  #查看输出内容 从1~n
    ql.run()