初探 qiling ( 麒麟 ):开源的二进制分析、高级代码模拟框架

news2024/11/28 22:44:54

官方介绍:

  • 官网:https://qiling.io/
  • :https://twitter.com/qiling_io
  • github 地址:https://github.com/qilingframework/qiling
     

1、qiling 简介

qiling 是什么

qiling 基于 python 开发,是一个开源的、可模拟多种架构和平台的二进制 仿真(模拟) 框架,同时还提供跨架构的调试能力,多种层次的 hook 方法,该工具由 Unicorn 引擎 驱动,并且拥有下列功能:

  • 模拟多平台:Windows,MacOS,Linux,Android,BSD,UEFI,DOS,MBR,以太坊虚拟机
  • 仿真多架构:8086、X86、X86_64、ARM、ARM64、MIPS、RISCV、PowerPC
  • 支持多种文件格式:PE,MachO,ELF,COM,MBR
  • 通过Demigod支持Windows驱动程序(.sys),Linux内核模块(.ko)和MacOS内核(.kext)
  • 在隔离环境中模拟 和 沙盒代码
  • 提供完全可配置的沙盒
  • 提供深度内存、寄存器、操作系统级别和文件系统级别 API
  • 细粒度显示:允许设置多种级别的钩子( instruction / basic-block / memory-access / exception / syscall / IO 等等);
  • 提供虚拟机级 API,例如保存和恢复当前执行状态。提供高级API来安装和配置沙盒环境;
  • 支持跨架构和平台调试功能
  • 具有反向调试功能的内置调试器
  • 允许动态修补运行中的程序代码,包括已加载的库;
  • Python 框架支持,允许构建和定制安全分析工具;

qiling 最基础的功能就是模拟,但是和 Unicorn 的模拟有所不同。

  • Unicorn 是一个 CPU 模拟器,
  • qiling 实际上是一个操作系统模拟器。

通常 "原生应用" 在一个 "特定的操作系统环境"上运行,这个操作系统会提供各种各样的 API,而操作系统运行在一个 CPU 之上。对于一个用 qiling 模拟的应用,架构是非常相似的:Unicorn 扮演了 CPU 模拟器的角色,qiling 来完成操作系统的任务执行目标二进制文件。"一个原生应用" 和 "一个模拟应用" 的联系。

为了实现这些,麒麟分为了三层。

  • os 层:提供了系统调用的实现,是最重要的部分。
  • loader 层,像真正的系统二进制加载器一样,解析目标二进制文件,设置内存布局,把代码和数据加载到内存里。
  • arch 层:进行一些和CPU架构相关的设置,比如:大小端,寄存器 等等。

Qiling 与其他 模拟器 对比

目前有很多开源模拟器,但最接近 Qiling 的两个项目​是 Unicorn & Qemu usermode 。

  • Unicorn ( 独角兽 ) engine:https://www.unicorn-engine.org/
  •  qemu:https://www.qemu.org/

Qemu usermode 与 Qiling 模拟器都是做类似的事情,即以跨架构的方式模拟整个可执行二进制文件。但是,Qiling 与 Qemu usermode 有一些重要的区别。

  • Qiling 是一个真正的分析框架,它允许你在上面构建自己的动态分析工具(用友好的Python语言)。同时,Qemu 只是一个工具,而不是一个框架
  • Qiling 可以执行动态检测,甚至可以在运行时热补丁代码。Qemu 无法做到
  • Qiling 不仅跨架构工作,而且是跨平台的,例如,您可以在 Windows 上运行 Linux ELF文件。相比之下,Qemu usermode 只运行相同操作系统的二进制文件,例如 Linux 上的 Linux ELF,因为它将系统调用从模拟代码转发到本机操作系统的方式。
  • Qiling 支持更多平台,包括 Windows、MacOS、Linux 和 BSD。Qemu usermode 只能处理 Linux 和 BSD

Qiling vs Unicorn引擎

Qiling ( 麒麟 ) 建立在 Unicorn ( 独角兽 ) 之上。但是它们两个完全不同:

  • Unicorn 只是一个CPU模拟器,它主要针对的是模拟CPU指令。Unicorn无法识别高级概念,例如动态库、系统调用、I/O处理或类似PE、MachO或ELF这样的可执行文件格式。因此,Unicorn只能够模拟原始设备指令,无法适用于操作系统上下文场景。
  • Qiling是一个高级框架,它可以利用Unicorn来模拟CPU指令,但是它同样可以理解操作系统上下文,它集成了可执行文件格式加载器、动态链接、系统调用和I/O处理器。更重要的是,Qiling可以在不需要原生操作系统的环境下运行可执行文件源码。

Qiling vs Qemu用户模式

Qemu用户模式 跟 Qiling类似,它可以跨架构模拟整个可执行文件的源码。但是,Qiling 的不同之处在于:

  • Qiling是一个真正的分析框架,它允许我们构建自己的动态分析工具(使用Python)。除此之外,Qemu只是一款工具,而不是一个框架。
  • Qiling可以执行动态指令,并且能够在运行时进行代码修补,这一点Qemu就无法做到了。
  • Qiling支持跨平台,但是Qemu用户模式只能在与源代码环境相同的操作系统上使用。
  • Qiling支持更多的平台,包括Windows、macOS、Linux&BSD,但Qemu用户模式只支持Linux&BSD。

逆向工程中优秀开源引擎框架: Capstone, Keystone, Unicorn, Qemu 以及 QiLing 等工具

汇编与反汇编

Capstone:新加坡南洋理工大学团队在 Blackhat USA 2014 上发布的一个反汇编引擎

  • Capstone 反汇编引擎 http://www.capstone-engine.org/
  • 项目主页:https://github.com/aquynh/capstone
  • 多平台 Windows、* nix
  • 多架构,例如 Arm、Mips 和 x86
  • 支持 C/Python 接口

Keystone:新加坡南洋理工大学团队在 Blackhat USA 2016 上发布的一个汇编框架

  • Capstone 汇编框架 https://www.keystone-engine.org/
  • 项目主页:https://github.com/keystone-engine/keystone
  • 多平台:Windows、* nix
  • 多架构,例如 Arm、Mips 和 x86
  • 支持 C/Python 接口

二进制模拟执行

QEMU:QEMU是一套由Fabrice Bellard所编写的模拟处理器的自由软件,一个通用的系统空间和用户空间仿真器和虚拟机

  • QEMU 虚拟机 https://www.qemu.org/
  • 项目主页:https://github.com/qemu/qemu
  • 多平台:Windows、* nix
  • 多架构,例如 Arm、Mips 和 x86
  • 支持 C/Python 接口

Unicorn:新加坡南洋理工大学团队在 Blackhat USA 2015 上发布的轻量级多平台,多体系结构的CPU仿真器框架 ( QEMU 提供了一个完整的仿真环境,既可以模拟硬件外设、整个系统,也可以模拟单个二进制程序。而 Unicorn 专注于 CPU 指令的仿真。 )

  • Unicorn 引擎 CPU仿真框架 https://www.unicorn-engine.org/
  • 项目主页:https://github.com/unicorn-engine/unicorn
  • 多平台:Windows、* nix
  • 多架构,例如 Arm、Mips 和 x86
  • 支持 C/Python 接口
  • 基于 QEMU

QiLing:京东团队在 Defcon 2019 上发布的高级二进制仿真框架。( Qiling 被设计为更高级别的框架,它利用 Unicorn 来模拟 CPU 指令,除此之外,还具有高级分析功能:执行动态检测,甚至可以在运行时热修补代码

  • Capstone 二进制仿真框架 https://www.qiling.io/
  • 项目主页:https://github.com/qilingframework/qiling
  • 多平台:Windows、* nix
  • 多架构,例如 Arm、Mips 和 x86
  • 基于 Unicorn

上面是二进制逆向的开源工具,都提供了详细的使用方法,并且有团队一直在维护,对于研究二进制病毒、恶意样本分析大有裨益。例如,

  • 修改固件、二进制,增加或者修改其中的指令,可以使用 Capstone/Keystone 进行汇编指令与二进制的转换
  • 跨平台仿真一些架构的二进制可执行文件, 可以使用 QEMU
  • 二进制指令片段的模拟,可以使用 Unicorn、QiLing

如果是正常的渗透测试,漏洞挖掘,CTF,这些工具可能用到的场合还是比较受限,因为更常用的工具是 IDA、Ghidra、pwntools,他们具有更好的集成性和扩展性。

qiling 官方文档

​官方文档:https://docs.qiling.io/en/latest/

qiling/extensions 目录下还有很多有用的插件。

Python 安装 qiling

安装:pip install qiling

安装最新版:pip install --user https://github.com/qilingframework/qiling/archive/dev.zip

注意:需要 Python 3.5 以上的版本

import 没问题就说明安装成功。

qiling 也支持 docker 化部署,直接 pull 一下就可以拿到一个独立的 qiling 运行环境了

docker pull qilingframework/qiling:latest

运行的时候需要挂载必要的一些目录

docker run -dt --name qiling \
-v /analysis/win/rootfs/x86_windows:/qiling/examples/rootfs/x86_windows \
-v /analysis/win/rootfs/x8664_windows:/qiling/examples/rootfs/x8664_windows \ 
qilingframework/qiling:latest

window、linux 相互执行 对方可执行文件

在 linux 下使用 qiling 框架执行 exe 文件

在 linux 上使用 qiling 运行 exe 时,需要模拟 windows系统环境(  window 的 dll 和 注册表 等组件 )才能运行。qiling 提供了一个脚本:在qiling/examples/scripts/dllscollector.bat,在 windows 上执行,就能得到全部所需要的依赖了

生成的依赖

如果需要其他 DLL,可以将 dll 手动复制到 "qiling/examples/rootfs/x8664_windows/Windows/System32" 或 "qiling/examples/rootfs/x86_windows/Windows/SysWOW64/" 具体依赖于程序体系结构。

示例代码:

from qiling import *

# sandbox to emulate the EXE
def my_sandbox(path, rootfs):
    # setup Qiling engine
    ql = Qiling(path, rootfs)
    # now emulate the EXE
    ql.run()

if __name__ == "__main__":
    # execute Windows EXE under our rootfs
    # 参数:可执行文件位置, 模拟的windows64所在位置
    my_sandbox(["x8664_hello.exe"], "examples/rootfs/x8664_windows")

在 windows 下使用 qiling 框架执行 linux 可执行文件

  • 在 linux 上执行 dylibcollector.sh 生成 linux 环境文件,然后把生成的文件放到 windows 上

Qltool 工具

文档:https://docs.qiling.io/en/latest/qltool/

Qiling 还提供了一个名为 qltool 的强大工具,可以快速模拟出目标 Shellcode 或可执行文件的源码。qltool 有三个可用的命令

运行选项

选项名称缩写参数描述
--filename-ffilename要模拟的二进制文件名
--rootfsdirname仿真根目录。这是所有库所在的位置
--args...模拟程序的命令行参数

注意:

  • 如果 --filename 没有指定, 最后一个参数将被视为 程序二进制。
  • 如果 --args 没有指定,则所有尾随参数将被视为程序命令行参数

代码选项

选项名缩写参数描述
--filename-ffilename输入文件名
--input-ihex输入十六进制字符串。仅当设置为--formathex
--formatasmhexbin指定文件或输入格式:程序集、十六进制字符串或二进制文件
--archx86x8664armarm_thumbarm64mipsa8086evm目标体系结构
--endianlittlebig目标字节序(默认值:little)
--oslinuxfreebsdmacoswindowsuefidosevm目标操作系统

注意:

  • 当 --format 设置为 hex 时, qltool 将首次在 --input 中查找数据。
  • 如果没有指定 --input , 将直接引用指定的 --filename

常用选项

选项名字缩写参数描述
--verbose-voffdefaultdebugdisasmdump设置日志记录详细级别
--envfilename包含环境字典的 Pickle 文件的路径,计算结果为字典的 Python 字符串
--gdb-g[server:port]启用 gdb 服务器
--qdb在入口点附加 qdb。目前仅支持 MIPS 和 ARM(拇指模式)
--rr启用qdb记录和重放功能;需要“--qdb”
--profilefilename指定配置文件
--no-console不要将程序输出发送到标准输出
--filter-eregexp对日志输出应用过滤正则表达式
--log-filefilename将日志发送到文件
--log-plain不要在日志输出中使用颜色;向文件发出日志时很有用
--rootEnable sudo required mode
--debug-stop在出现错误时停止仿真。requires verbose to be set to either debug or dump
--multithread-m在多线程模式下执行程序
--timeoutmicroseconds以微秒为单位设置仿真超时 (1000000μs = 1s)
--coverage-file-cfilename代码覆盖率输出文件
--coverage-formatdrcovdrcov_exact代码覆盖率文件格式
--json发出 JSON 格式的仿真报告

qltui --- qltool 的终端用户界面

qltui:$ ./qltool qltui

qltool 官方文档示例

生成 shellcode

$ ./qltool code --os linux --arch arm --format hex -f examples/shellcodes/linarm32_tcp_reverse_shell.hex

$ ./qltool code --os linux --arch x86 --format asm -f examples/shellcodes/lin32_execve.asm

对目标源码进行模拟:
$ ./qltool run -f examples/rootfs/arm_linux/bin/arm32-hello --rootfs examples/rootfs/arm_linux/
如需模拟Shellcode,则需要运行下列命令:
$ ./qltool shellcode --os linux --arch x86 --asm -f examples/shellcodes/lin32_execve.asm 

运行 二进制文件:

$ ./qltool run -f examples/rootfs/x8664_linux/bin/x8664_hello --rootfs  examples/rootfs/x8664_linux/
$ ./qltool run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux

UEFI file

$ ./qltool run -f examples/rootfs/x8664_efi/bin/TcgPlatformSetupPolicy --rootfs examples/rootfs/x8664_efi --env examples/rootfs/x8664_efi/rom2_nvar.pickel

使用代码覆盖率收集(目前仅限UEFI):
$ ./qltool run -f examples/rootfs/x8664_efi/bin/TcgPlatformSetupPolicy --rootfs examples/rootfs/x8664_efi --coverage-format drcov --coverage-file TcgPlatformSetupPolicy.cov

启用 GDB 调试器时:

$ ./qltool run -f examples/rootfs/x8664_linux/bin/x8664_hello --gdb 127.0.0.1:9999 --rootfs examples/rootfs/x8664_linux

二进制文件 和 参数

$ ./qltool run -f examples/rootfs/x8664_linux/bin/x8664_args --rootfs examples/rootfs/x8664_linux --args test1 test2 test3
$ ./qltool run --rootfs examples/rootfs/x8664_linux examples/rootfs/x8664_linux/bin/x8664_args test1 test2 test3

二进制文件 和 输出格式

$ ./qltool run -f examples/rootfs/mips32el_linux/bin/mips32el_hello --rootfs examples/rootfs/mips32el_linux --verbose disasm

二进制文件 和 环境

$ ./qltool run -f jexamples/rootfs/x8664_linux/bin/tester --rootfs jexamples/rootfs/x8664_linux --env '{"LD_PRELOAD":"hijack.so"}' --verbose debug

json 输出(主要是Windows):

$ ./qltool run -f examples/rootfs/x86_windows/bin/x86_hello.exe --rootfs  examples/rootfs/x86_windows/ --console False --json

简单使用 Qiling 框架

:https://forum.butian.net/share/1482

qiling 可以模拟各种架构和系统的架构。是一个二进制程序模拟框架,和 unicorn 类似。可以这样说,qiling 是 unicorn 的二次开发。他能够跨平台使用,在windows,macos,linux 等下都可以,支持各种架构,能够模拟多种类型的文件,如 ELF、exe。

基本使用:官方 demo

from qiling import Qiling
from qiling.const import QL_VERBOSE

# 提前写好要执行的shellcode,fromhex是将十六进制数变成\x的形式
shellcode = bytes.fromhex('''
fc4881e4f0ffffffe8d0000000415141505251564831d265488b52603e488b52183e488b52203e488b72503e480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed5241513e488b52203e8b423c4801d03e8b80880000004885c0746f4801d0503e8b48183e448b40204901d0e35c48ffc93e418b34884801d64d31c94831c0ac41c1c90d4101c138e075f13e4c034c24084539d175d6583e448b40244901d0663e418b0c483e448b401c4901d03e418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a3e488b12e949ffffff5d49c7c1000000003e488d95fe0000003e4c8d850f0100004831c941ba45835607ffd54831c941baf0b5a256ffd548656c6c6f2c2066726f6d204d534621004d657373616765426f7800
''')

# 执行的code,设置架构和系统,其实就是实例化程序
ql = Qiling(
    code=shellcode,
    rootfs=r'examples/rootfs/x8664_windows',
    archtype='x8664', ostype='Windows',
    verbose=QL_VERBOSE.DEBUG
)

# 运行
ql.run()

Qiling 初始化 constructor 可以采用多个参数:

  • filename:二进制文件及其参数,例如:filename = ["test", "-argv1", "argv2"]
  • rootfs:虚拟 根(/) 文件夹,这是执行 Qiling(目标体系结构)时的 jail 文件系统
  • env:环境变量,例如:env = {"SHELL": "/bin/bash", "HOME": "/tmp"}
  • output:“默认”,“调试”,“ disasm”,“ dump”,其中dump =(disam + debug)

该 run() 函数还可以采用多个参数:

  • begin:仿真代码的起始地址
  • end:仿真代码的结束地址
  • timeout:仿真超时(以微秒为单位)
  • count:要模拟的最大指令数

Qiling 使用方法 (栈、寄存器、内存、hook)

参考官方文档:https://docs.qiling.io/en/latest/

1.对栈的操作

ql.stack_push()                     # 模拟程序中进行压栈操作
ql.stack_read(offset)             # 从栈中读出数据
ql.stack_write(offset,data)     # 向栈中写入数据

2.对寄存器的操作

# 旧的使用方式,从 eax 中读: demo = ql.reg.eax

demo = ql.arch.regs.read("eax")
print(demo)  # 输出寄存器的信息

3.对内存的操作

ql.mem.read(address, size)    # 从 address 中读出 size 字节的数据
ql.mem.write(address, data)    # 向 address 里写入 data

4.hook

1.对地址的 hook

from qiling import Qiling


def stop(ql: Qiling) -> None:
    ql.log.info('killer switch found, stopping')
    # # 当程序执行到 0x40819a 处时执行 stop 函数
    ql.emu_stop()


ql = Qiling(
    [r'examples/rootfs/x86_windows/bin/wannacry.bin'],
    r'examples/rootfs/x86_windows'
)
ql.hook_address(stop, 0x40819a)
ql.run()

2.对代码的 hook

from capstone import Cs
from qiling import Qiling
from qiling.const import QL_VERBOSE


def simple_disassembly(ql: Qiling, address: int, size: int, md: Cs) -> None:
    # 读取内存中指定地址、指定长度 的 十六进制的code
    buf = ql.mem.read(address, size)

    # 将十六进制数转变成汇编代码,并且输出
    for insn in md.disasm(buf, address):
        ql.log.debug(f':: {insn.address:#x} : {insn.mnemonic:24s} {insn.op_str}')


if __name__ == "__main__":
    ql = Qiling(
        # [r'examples/rootfs/x8664_linux/bin/x8664_hello'],
        # r'examples/rootfs/x8664_linux',
        [r'D:\Software\qiling-master\examples\rootfs\x8664_linux\bin\x8664_hello'],
        r'D:\Software\qiling-master\examples\rootfs\x8664_linux',
        verbose=QL_VERBOSE.DEBUG
    )
    # 代码 hook
    ql.hook_code(simple_disassembly, user_data=ql.arch.disassembler)
    ql.run()

ql.hook_code 是对 read 的十六进制数进行 hook,后面通过题目来讲,通俗来说就是执行每条汇编语句之前都会先执行 simple_diassembler

qiling 专用 调试器 --- Qdb

需要自行安装:https://github.com/ucgJhe/Qdb

2、初探二进制分析框架 qiling

示例:crackme_linux

From :https://zhuanlan.zhihu.com/p/369023397

示例:qiling/examples/rootfs/x86_linux/bin/crackme_linux

简单看一下题目

就是通过 scanf 函数往 bss 段上输入一些内容,当这些内容满足 sub_8048451函数 里面的约束时,就可以调用 ok 函数拿到 flag

这里的约束也很简单,无非就是一些指定某某值要等于某某值,然后就是一些异或运算,手算也很快就能得到结果,这里用 qiling 来解题 ,具体操作可以看下面的脚本。

运行结果如下:

由此我们可以得到 flag 就是 L1NUX了

Qiling Framework 入门,11个挑战快速上手

From:https://zhuanlan.zhihu.com/p/405559684

QilingLab : Shielder - QilingLab – Release,是一个包含十几个小挑战的程序,用于快速上手Qiling 框架的主要功能。aarch64版本已有Joan Sivion做的Qiling Labs - JoanSivion Security Blog,这里用x86-64做一遍,当作对Joan Sivion的补充和中文翻译。

qilinglab 出发点只是训练 qiling 框架的使用,所以没有啥逆向强度,符号也没有去,可以直接用IDA打开看即可。

题目列表

Challenge 1: Store 1337 at pointer 0x1337.
Challenge 2: Make the 'uname' syscall return the correct values.
Challenge 3: Make '/dev/urandom' and 'getrandom' "collide".
Challenge 4: Enter inside the "forbidden" loop.
Challenge 5: Guess every call to rand().
Challenge 6: Avoid the infinite loop.
Challenge 7: Don't waste time waiting for 'sleep'.
Challenge 8: Unpack the struct and write at the target address.
Challenge 9: Fix some string operation to make the iMpOsSiBlE come true.
Challenge 10: Fake the 'cmdline' line file to return the right content.
Challenge 11: Bypass CPUID/MIDR_EL1 checks.

IDA 打开,可以看到 main() ---> start() 里就是一大堆调用 challangeX(a) 并对结果进行校验,我们的目的就是让传入的指针指向的位置能被正确赋值1。没有逆向难度所以不多做解释。
直接运行的话,并不会输出每个Challenge的结果(SOLVED/UNSOLVED),甚至还会出现segment fault,这些都是正常现象,请放心食用,当你完成某些挑战后,自然就能看到更多的结果了。

基本用法

因为需要用到 rootfs 里的东西,所以把 qiling 的 GitHub 仓库 clone 下来,里面 rootfs 是个submodule。git clone https://github.com/qilingframework/qiling.git --recursiv

from qiling import *

def challenge1(ql: Qiling):
    pass

if __name__  == '__main__':
    path = ['qilinglab-x86_64'] # 我们的目标
    rootfs = "./qiling/examples/rootfs/x8664_linux" # 在你clone下来的仓库里
    ql = Qiling(path, rootfs)
    challenge1(ql) # 在ql.run()之前,做好我们的hook工作
    ql.run()

challenge 1: 操作内存

_BYTE *__fastcall challenge1(_BYTE *a1)
{
  _BYTE *result; // rax

  result = (_BYTE *)MEMORY[0x1337];
  if ( MEMORY[0x1337] == 1337 )
  {
    result = a1;
    *a1 = 1;
  }
  return result;
}

需要我们让内存0x1337上存放一个值为1337,我们其实并不能保证程序加载基地址,这里我们需要用:ql.mem.map(0x1000, 0x1000, info='[challenge]')

映射一块内存,需要注意的是,qiling底层就是用的Unicorn Engine,内存映射时,要4k对齐。综上,还是挺容易得出第一处解法。

脚本。( 在ql.run()前加一句ql.verbose = 0方便看输出内容 )

def challenge1(ql: Qiling):
    ql.mem.map(0x1000, 0x1000, info='[challenge1]')
    ql.mem.write(0x1337, ql.pack16(1337)) # pack16(value) == struct.pack('H', value)

challenge 2: 修改系统调用

让我们修改uname系统调用,让它返回正确的值。IDA看下“正确的值”指的是什么

unsigned __int64 __fastcall challenge2(_BYTE *a1)
{
  unsigned int v2; // [rsp+10h] [rbp-1D0h]
  int v3; // [rsp+14h] [rbp-1CCh]
  int v4; // [rsp+18h] [rbp-1C8h]
  int v5; // [rsp+1Ch] [rbp-1C4h]
  struct utsname name; // [rsp+20h] [rbp-1C0h] BYREF
  char s[10]; // [rsp+1A6h] [rbp-3Ah] BYREF
  char v8[16]; // [rsp+1B0h] [rbp-30h] BYREF
  unsigned __int64 v9; // [rsp+1C8h] [rbp-18h]

  v9 = __readfsqword(0x28u);
  if ( uname(&name) )
  {
    perror("uname");
  }
  else
  {
    strcpy(s, "QilingOS");
    strcpy(v8, "ChallengeStart");
    v2 = 0;
    v3 = 0;
    while ( v4 < strlen(s) )
    {
      if ( name.sysname[v4] == s[v4] )
        ++v2;
      ++v4;
    }
    while ( v5 < strlen(v8) )
    {
      if ( name.version[v5] == v8[v5] )
        ++v3;
      ++v5;
    }
    if ( v2 == strlen(s) && v3 == strlen(v8) && v2 > 5 )
      *a1 = 1;
  }
  return __readfsqword(0x28u) ^ v9;
}

uname系统调用用来获取系统的一些信息,传入一个utsname结构体buffer让它填充。可以在<sys/utsname.h>看到utsname的定义(经过整理):

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

而Qiling提供了在系统调用返回时进行hook的功能。

脚本

def hook_uname_on_exit(ql: Qiling, *args):
    rdi = ql.reg.rdi
    ql.mem.write(rdi, b'QilingOS\x00')
    ql.mem.write(rdi + 65 * 3, b'ChallengeStart\x00')
​
def challenge2(ql: Qiling):
    ql.set_syscall('uname', hook_uname_on_exit, QL_INTERCEPT.EXIT)

challenge 3: 劫持文件系统&系统调用

unsigned __int64 __fastcall challenge3(_BYTE *a1)
{
  int v2; // [rsp+10h] [rbp-60h]
  int i; // [rsp+14h] [rbp-5Ch]
  int fd; // [rsp+18h] [rbp-58h]
  char v5; // [rsp+1Fh] [rbp-51h] BYREF
  char buf[32]; // [rsp+20h] [rbp-50h] BYREF
  char buf2[40]; // [rsp+40h] [rbp-30h] BYREF
  unsigned __int64 v8; // [rsp+68h] [rbp-8h]

  v8 = __readfsqword(0x28u);
  fd = open("/dev/urandom", 0);
  read(fd, buf, 0x20uLL);
  read(fd, &v5, 1uLL);
  close(fd);
  getrandom((__int64)buf2, 32LL, 1LL);
  v2 = 0;
  for ( i = 0; i <= 31; ++i )
  {
    if ( buf[i] == buf2[i] && buf[i] != v5 )
      ++v2;
  }
  if ( v2 == 32 )
    *a1 = 1;
  return __readfsqword(0x28u) ^ v8;
}

可以看到需要我们解决两个问题:

  • 直接读取/dev/urandom得到的随机数,和通过getrandom()得到的随机数要完全一样
  • 还要有一个字节的随机数,和剩下的都不一样。

Qiling提供了QlFsMappedObject让我们能很方便地自定义文件系统,最少要实现close,剩下的可以看源码,根据需要自行去实现:read, write, fileno, lseek, fstat, ioctl, tell, dup, readline。

脚本

class Fake_urandom(QlFsMappedObject):
    def read(self, expected_len):
        if expected_len == 1:
            return b'\x23'  # casual single byte here
        else:
            return b'\x00' * expected_len

    def close(self):
        return 0


def hook_getrandom(ql: Qiling, buf, buflen, flags, *args):
    ql.mem.write(buf, b'\x00' * buflen)
    ql.os.set_syscall_return(0)


def challenge3(ql: Qiling):
    ql.add_fs_mapper('/dev/urandom', Fake_urandom())
    ql.set_syscall('getrandom', hook_getrandom)

challenge 4: hook地址

脚本
hook_address里注册的回调在执行被hook地址处之前执行,然后才执行这个地址上的指令。所以我们hook在cmp这句。这样while里就是比较 i < 1了。

def enter_forbidden_loop_hook(ql: Qiling):
    ql.reg.eax = 1
​
def challenge4(ql: Qiling):
    """
    000055A3E4800E40 8B 45 F8   mov     eax, [rbp+var_8]
    000055A3E4800E43 39 45 FC   cmp     [rbp+var_4], eax        <<< hook here
    000055A3E4800E46 7C ED      jl      short loc_55A3E4800E35
    """
    base = ql.mem.get_lib_base(ql.path)
    hook_addr = base + 0xE43
    ql.hook_address(enter_forbidden_loop_hook, hook_addr)

challenge 5: hook外部函数

来看目标:

unsigned __int64 __fastcall challenge5(_BYTE *a1)
{
  unsigned int v1; // eax
  int i; // [rsp+18h] [rbp-48h]
  int j; // [rsp+1Ch] [rbp-44h]
  int v5[14]; // [rsp+20h] [rbp-40h]
  unsigned __int64 v6; // [rsp+58h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  v1 = time(0LL);
  srand(v1);
  for ( i = 0; i <= 4; ++i )
  {
    v5[i] = 0;
    v5[i + 8] = rand();
  }
  for ( j = 0; j <= 4; ++j )
  {
    if ( v5[j] != v5[j + 8] )
    {
      *a1 = 0;
      return __readfsqword(0x28u) ^ v6;
    }
  }
  *a1 = 1;
  return __readfsqword(0x28u) ^ v6;
}

显然,只要让rand()每次都返回0是最简单的修改方法了。所以代码比较简单:

脚本

def hook_rand(ql: Qiling):
    ql.reg.rax = 0
​
def challenge5(ql: Qiling):
    ql.set_api('rand', hook_rand)

看不到输出challenge5: SOLVED是正常的,因为卡在了challenge6,没有输出来信息。

challenge 6: 突破死循环

流程很清晰,只要我们修改死循环部分,参数就会乖乖被赋值1

脚本,同理,要抢在比较之前修改al的值

def hook_while_true(ql: Qiling):
    """
    0000564846E00F12 0F B6 45 FB    movzx   eax, [rbp+var_5]
    0000564846E00F16 84 C0          test    al, al          <<< hook here
    0000564846E00F18 75 F1          jnz     short loc_564846E00F0B
    """
    ql.reg.rax = 0
​
def challenge6(ql: Qiling):
    base = ql.mem.get_lib_base(ql.path)
    ql.hook_address(hook_while_true, base + 0xF16)

challenge 7: 不许睡了,快起来!——修改sleep()

unsigned int __fastcall challenge7(_BYTE *a1)
{
  *a1 = 1;
  return sleep(0xFFFFFFFF);
}

可以很轻易想到n种办法:

  1. 修改sleep参数值,减少睡眠时间
  2. 自己实现apisleep(),直接返回
  3. 修改系统调用nanosleep(),直接返回,因为sleep()底层调用的nanosleep()

可以看一下

# man 3 sleep
NOTES
       On Linux, sleep() is implemented via nanosleep(2).   See  the  nanosleep(2)
       man page for a discussion of the clock used. 

脚本

def modify_arg(ql: Qiling):
    ql.reg.edi = 0
​
def i_slept_i_faked_it(ql: Qiling):
    # 我睡了,我装的
    return
​
def hook_nanosleep(ql: Qiling, *args, **kwargs):
    # 注意参数列表
    return
​
def challenge7(ql: Qiling):
    # method 1
    # ql.set_api('sleep', modify_arg, QL_INTERCEPT.ENTER)
    # method 2
    # ql.set_api('sleep', i_slept_i_faked_it)
    # method 3
    ql.set_syscall('nanosleep', hook_nanosleep)

challenge 8: 解析结构体,往正确地址写入值

这里直接借用Joan Sivion整理的代码:

void challenge8(char *check) {
    random_struct *s;

    s = (random_struct *)malloc(24);
    s->some_string = (char *)malloc(0x1E);
    s->magic = 0x3DFCD6EA00000539;
    strcpy(s->field_0, "Random data");
    s->check_addr = check;
}

struct random_struct {
  char *some_string;
  __int64 magic;
  char *check_addr;
};

方法有两种:

  • 1. 最后一句的时候,获取这个结构体地址,可以看一下最后部分的汇编:

.text:0000564846E00FA9 48 8B 45 F8      mov     rax, [rbp+stru] ; stru == -8
.text:0000564846E00FAD 48 8B 55 E8      mov     rdx, [rbp+var_18]
.text:0000564846E00FB1 48 89 50 10      mov     [rax+10h], rdx
.text:0000564846E00FB5 90               nop
.text:0000564846E00FB6 C9               leave
.text:0000564846E00FB7 C3               retn

然后结构体偏移+0x16的地方就是保存了参数的位置。

  • 使用qiling提供的机制,用ql.mem.search()搜索内存。看一下代码可以发现,这个结构体保存了一个魔数0x3DFCD6EA00000539,这正好是个特征方便我们去搜索这块内存。当然,还在第一个字段保存了一个指针指向一个固定的字符串,如果那个魔数在内存中出现多次,那显然我们还需要进一步获取信息确定是不是我们要找的那个结构体:保存了参数check的结构体。

脚本

def hook_struct(ql: Qiling):
    """
    0000564846E00FA9 48 8B 45 F8      mov     rax, [rbp+str]    ; rbp - 8
    0000564846E00FAD 48 8B 55 E8      mov     rdx, [rbp+var_18]
    0000564846E00FB1 48 89 50 10      mov     [rax+10h], rdx
    0000564846E00FB5 90               nop       <<< hook here
    0000564846E00FB6 C9               leave
    0000564846E00FB7 C3               retn
    """
    heap_struct_addr = ql.unpack64(ql.mem.read(ql.reg.rbp - 8, 8))
    heap_struct = ql.mem.read(heap_struct_addr, 24)
    printHex(heap_struct)
    _, _, check_addr = struct.unpack('QQQ', heap_struct)
    ql.mem.write(check_addr, b'\x01')
​
def search_mem_to_find_struct(ql: Qiling):
    MAGIC = ql.pack64(0x3DFCD6EA00000539)
    candidate_addrs = ql.mem.search(MAGIC)
​
    for addr in candidate_addrs:
        # 有可能有多个地址,所以通过其他特征进一步确认
        stru_addr = addr - 8
        stru = ql.mem.read(stru_addr, 24)
        string_addr, _, check_addr = struct.unpack('QQQ', stru)
        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)
    # method 1
    # ql.hook_address(hook_struct, base + 0xFB5)
    # method 2
    ql.hook_address(search_mem_to_find_struct, base + 0xFB5)

challenge 9: 修改字符串函数

unsigned __int64 __fastcall challenge9(bool *a1)
{
  char *i; // [rsp+18h] [rbp-58h]
  char dest[32]; // [rsp+20h] [rbp-50h] BYREF
  char src[40]; // [rsp+40h] [rbp-30h] BYREF
  unsigned __int64 v5; // [rsp+68h] [rbp-8h]

  v5 = __readfsqword(0x28u);
  strcpy(src, "aBcdeFghiJKlMnopqRstuVWxYz");
  strcpy(dest, src);
  for ( i = dest; *i; ++i )
    *i = tolower(*i);
  *a1 = strcmp(src, dest) == 0;
  return __readfsqword(0x28u) ^ v5;
}

要么让tolower()失效,要么让strcmp()失效。

脚本

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

challenge10: 劫持文件系统,返回指定命令行

unsigned __int64 __fastcall challenge10(_BYTE *a1)
{
  int i; // [rsp+10h] [rbp-60h]
  int fd; // [rsp+14h] [rbp-5Ch]
  ssize_t v4; // [rsp+18h] [rbp-58h]
  char buf[72]; // [rsp+20h] [rbp-50h] BYREF
  unsigned __int64 v6; // [rsp+68h] [rbp-8h]

  v6 = __readfsqword(0x28u);
  fd = open("/proc/self/cmdline", 0);
  if ( fd != -1 )
  {
    v4 = read(fd, buf, 0x3FuLL);
    if ( v4 > 0 )
    {
      close(fd);
      for ( i = 0; v4 > i; ++i )
      {
        if ( !buf[i] )
          buf[i] = 32;
      }
      buf[v4] = 0;
      if ( !strcmp(buf, "qilinglab") )
        *a1 = 1;
    }
  }
  return __readfsqword(0x28u) ^ v6;
}

让我们能从/proc/self/cmdline读到"qilinglab",故技重施即可,hook文件系统。

脚本

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())

简便方法,可以直接替换指定文件成我们的文件,先创建我们需要的文件:

echo -n "qilinglab" > fake_cmdline

然后代码就可以这样写了:

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

还一种方法,代码也不用写了,直接往qiling的rootfs里面写:

mkdir -p ./qiling/examples/rootfs/x8664_linux/proc/self
echo -n "qilinglab" > ./qiling/examples/rootfs/x8664_linux/proc/self/cmdline

challenge 11: 指令hook

这里指的是用qiling的指令hook_code()而不是ql.hook_insn(),先看下目标

unsigned __int64 __fastcall challenge11(_BYTE *a1)
{
  int v7; // [rsp+1Ch] [rbp-34h]
  int v8; // [rsp+24h] [rbp-2Ch]
  char s[4]; // [rsp+2Bh] [rbp-25h] BYREF
  char v10[4]; // [rsp+2Fh] [rbp-21h] BYREF
  char v11[4]; // [rsp+33h] [rbp-1Dh] BYREF
  unsigned __int64 v12; // [rsp+38h] [rbp-18h]

  v12 = __readfsqword(0x28u);
  _RAX = 0x40000000LL;
  __asm { cpuid }
  v7 = _RCX;
  v8 = _RDX;
  if ( __PAIR64__(_RBX, _RCX) == 0x696C6951614C676ELL && (_DWORD)_RDX == 538976354 )
    *a1 = 1;
  // ...
}

看下汇编:

cpuid
mov     eax, edx
mov     esi, ebx
mov     [rbp+var_30], esi
mov     [rbp+var_34], ecx
mov     [rbp+var_2C], eax
cmp     [rbp+var_30], 696C6951h
jnz     short loc_564846E011C0
cmp     [rbp+var_34], 614C676Eh
jnz     short loc_564846E011C0
cmp     [rbp+var_2C], 20202062h
jnz     short loc_564846E011C0

目标就很明确了。cpuid指令会填充几个寄存器,具体可以参考intel手册。

脚本

def hook_cpuid(ql: Qiling, address, size):
    """
    0000564846E0118F 0F A2      cpuid
    """
    if ql.mem.read(address, size) == b'\x0F\xA2':
        ql.reg.ebx = 0x696C6951
        ql.reg.ecx = 0x614C676E
        ql.reg.edx = 0x20202062
        ql.reg.rip += 2
​
​
def challenge11(ql: Qiling):
    begin, end = 0, 0
    for info in ql.mem.map_info:
        if info[2] == 5 and info[3] == '/mnt/d/Playground/QilingLab/qilinglab-x86_64':
            begin, end = info[:2]
​
    ql.hook_code(hook_cpuid, begin=begin, end=end)

说明:ql.mem.map_info也就是ql.mem.show_mapinfo()的内容,5表示的是r-x属性,加这个判断也是为了缩小hook的范围,提高性能。

基于 qiling 的各种姿势

官方做了一个收集,里面各种使用了qiling框架的工具,姿势都会被收录其中,很多东西做的还是针不戳的

:https://github.com/qilingframework/qiling/issues/134

有人基于此写了fuzz,如efi_fuzz,有的人写了使用qiling复现漏洞的过程,还有人用qiling模拟运行了路由器的固件,还有用于ctf逆向的各种骚操作

Qiling框架模拟运行固件配合IDA动态调试

:https://bbs.kanxue.com/thread-274828.htm

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/364059.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vue3 基础

Vue3 基础 概述 Vue (发音为 /vjuː/&#xff0c;类似 view) 是一款用于构建用户界面的 JavaScript 框架。它基于标准 HTML、CSS 和 JavaScript 构建&#xff0c;并提供了一套声明式的、组件化的编程模型&#xff0c;帮助你高效地开发用户界面。无论是简单还是复杂的界面&…

升职加薪必备,2023年程序员不能不知道的AI辅助编码工具

已经有很多人把chatGPT当做必备的Bug修复工具了&#xff0c;对于用AI写代码&#xff0c;有人感到失落&#xff0c;害怕被取代&#xff0c;而另一些人则认为人工智能将加快编写更好代码的过程。 尽管 AI 编写的代码并非完美无缺&#xff0c;但我相信&#xff0c;最终AI将取代人…

Java实例——网络实例

1、主机IP地址获取 步骤一&#xff1a;获取主机InetAddress 步骤二&#xff1a;获取主机IP地址 InetAddress address null;try {address InetAddress.getByName("www.baidu.com");}catch (UnknownHostException e) {System.exit(2);}System.out.println("Host…

聚类(性能度量)

文章目录聚类&#xff08;性能度量&#xff09;外部指标例1内部指标例2聚类&#xff08;性能度量&#xff09; 对数据集 D{x1,x2,...,xm}D\{x_1,x_2,...,x_m\}D{x1​,x2​,...,xm​} &#xff0c;假定通过聚类给出的簇划分为 C{C1,C2,...,Ck}C\{C_1,C_2,...,C_k\}C{C1​,C2​,…

计算机组成原理错题

静态RAM&#xff08;SRAM&#xff09;和动态RAM&#xff08;DRAM&#xff09;的基本电路图不同&#xff0c;因此可以通过观察存储器的基本电路图来判断它属于哪一类。 静态RAM的基本电路图包括一个存储单元和一个数据选择器。每个存储单元由一个触发器&#xff08;flip-flop&a…

汽车零部件企业数字工厂管理系统建设方案

在汽车零部件制造领域&#xff0c;伴随工业信息化与机器人化&#xff0c;制造模式逐渐从 CAD/CAE/CAM 数字化设计及加工走向全产品周期虚拟现实的数字化工厂管理系统平台&#xff0c;实现虚拟现实设计制造&#xff0c;防范产品缺陷并预防设备故障&#xff0c;大幅提高生产效率。…

做出选择,直面挑战,揭开数据中心网络的发展真相

为什么&#xff1f;你们发现没有&#xff1f;不知道&#xff0c;从什么时候开始&#xff0c;这个世界&#xff0c;变得越来越快了。快得仿佛昨天刚刚来到这个世界&#xff0c;一眨眼就日暮西山了。是的&#xff0c;时间过得好快&#xff0c;回想起2002年7月电气和电子工程师协会…

炼石:八年饮冰难凉热血,初心如磐百炼成钢

炼石成立八周年 八载笃行&#xff0c;踔厉奋发。创立于2015年的炼石&#xff0c;今天迎来了八岁生日&#xff0c;全体员工共同举行了温暖又充满仪式感的周年庆典。过去的2022&#xff0c;是三年疫情的艰难“收官之年”&#xff0c;新的2023&#xff0c;将是数据安全行业成为独…

FFT的物理意义

FFT结果的物理意义 FFT是离散傅立叶变换的快速算法&#xff0c;可以将一个信号变换到频域。有些信号在时域上是很难看出什么特征的&#xff0c;但是如果变换到频域之后&#xff0c;就很容易看出特征了。这 就是很多信号分析采用FFT变换的原因。另外&#xff0c;FFT可以将一…

内网渗透(四十九)之域控安全和跨域攻击-多种方式离线读取ntds.dit文件中的hash值

系列文章第一章节之基础知识篇 内网渗透(一)之基础知识-内网渗透介绍和概述 内网渗透(二)之基础知识-工作组介绍 内网渗透(三)之基础知识-域环境的介绍和优点 内网渗透(四)之基础知识-搭建域环境 内网渗透(五)之基础知识-Active Directory活动目录介绍和使用 内网渗透(六)之基…

Java方法【未完待续】

目录 前言 一、什么是方法&#xff1f; 二、方法的定义和调用 方法的定义 方法的调用 三、方法的重载 重载规则 实现理论 总结 前言 随着对Java这一编程语言的深入学习&#xff0c;大家可能会遇到一个熟悉又陌生的词——方法&#xff0c;其实Java方法就是我们学习C/C时…

2023该好好赚钱了,推荐三个下班就能做的副业

在过去的两年里&#xff0c;越来越多的同事选择辞职创业。许多人通过互联网红利赚到了他们的第一桶金。随着短视频的兴起&#xff0c;越来越多的人吹嘘自己年收入百万&#xff0c;导致很多刚进入职场的年轻人逐渐迷失自我&#xff0c;认为钱特别容易赚。但事实上&#xff0c;80…

构造agent类型的内存马(内存马系列篇十三)

写在前面 前面我们对JAVA中的Agent技术进行了简单的学习&#xff0c;学习前面的Agent技术是为了给这篇Agent内存马的实现做出铺垫&#xff0c;接下来我们就来看看Agent内存马的实现。 这是内存马系列篇的第十三篇了。 环境搭建 我这里就使用Springboot来搭建一个简单的漏洞…

电脑病毒已灭绝,是真的吗?

大家有没有这样一个疑问&#xff0c;觉得自己的电脑好像很久没有电脑病毒了&#xff1f;之前大名鼎鼎的蠕虫2000&#xff0c;熊猫烧香都变得不那么常见了。到底是电脑因为自身优化和杀毒软件的防护导致病毒变少了&#xff0c;还是本身电脑病毒变少了呢&#xff1f;&#xff08;…

Boost库文档搜索引擎

文章目录综述效果展示去标签化&#xff0c;清理数据构建索引用户查询综述 该项目使用了BS架构&#xff0c;实现了用户对Boost库进行站内搜索的功能&#xff0c; 用户输入关键字使用http协议通过ajax将数据发送给后端服务器&#xff0c;后端进行分词&#xff0c; 通过倒排索引…

【Kubernetes】第七篇 - Service 服务介绍和使用

一&#xff0c;前言 上一篇&#xff0c;通过配置一个 Deployment 对象&#xff0c;在内部创建副本集对象&#xff0c;副本集帮我们创建了 3 个 pod 副本 由于 pod 存在 IP 漂移现象&#xff0c;pod 的创建和重启会导致 IP 变化&#xff1b; 本篇&#xff0c;介绍 Service 服…

《计算机网络:自顶向下方法》实验5:NAT协议分析 Wireshark实验

实验12:NAT协议分析 1 What is the IP address of the client? 客户端的 IP 地址是192.168.1.100 2 The client actually communicates with several different Google servers in order to implement “safe browsing.” (See extra credit section at the end of this la…

Safety-Gym环境配置与安

官网&#xff1a; https://github.com/openai/safety-gym https://github.com/openai/safety-starter-agents 一、安装依赖环境配置 建议使用python 3.7及以下环境&#xff0c;因为官方的safety-rl是基于tensorflow1.13.1实现&#xff0c;而tensorflow1.13.1只能支持python…

leaflet 自定义添加地图网格线(087)

第087个 点击查看专栏目录 本示例的目的是介绍演示如何在vue+leaflet中自定义添加地图网格线。 直接复制下面的 vue+openlayers源代码,操作2分钟即可运行实现效果. 文章目录 示例效果配置方式示例源代码(共76行)安装插件相关API参考:专栏目标示例效果 配置方式 1)查看基…

前端学习第九站——Vue3基础篇

目录 一、环境搭建 创建项目 编码 IDE 修改端口 配置代理 项目架构 二、Vue组件 main.ts 属性绑定 事件绑定 表单绑定 计算属性 xhr axios 环境变量 baseURL 拦截器 条件和列表 监听器 vueuse useRequest usePagination&#xff08;分页&#xff09; 子组…