
qiling麒麟逆向框架搭建使用
本文最后更新于 2025-03-15,文章内容可能已经过时。
qiling开源逆向框架安装和使用
转载
版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
原文链接:https://blog.csdn.net/Refrain_mh/article/details/128673116
qiling开源逆向框架介绍:
qiling是一个开源的二进制分析框架
github地址:
https://github.com/qilingframework/qiling
官网官方介绍:
https://docs.qiling.io/en/latest/
qiling是一个可模拟多种架构和平台的模拟执行框架,基于Unicorn
框架开发而来,可支持的平台有:Windows, MacOS, Linux, BSD, UEFI, DOS,可支持的架构有: X86,
X86_64, Arm, Arm64, MIPS,
8086,同时还提供跨架构的调试能力,多种层次的hook方法,qiling基于python开发,上手使用起来也非常方便,学习成本低
然而,麒麟框架不是旨于构建另一个“沙盒”工具,而是为逆向工程设计的框架。因此,二进制检测和API是麒麟框架的主要及优先关注点。使用麒麟框架可以节省时间。拥有丰富API的麒麟框架将逆向工程及二进制代码检测快速的提升到了一个新的层次。
此外,麒麟框架还提供了对寄存器、内存、文件系统、操作系统和调试器的API访问。麒麟框架也提供了虚拟机级别的API,如保存和恢复执行状态。
QilingLab:
是一个包含十几个小挑战的程序,用于快速上手Qiling框架的主要功能。
https://www.shielder.com/blog/2021/07/qilinglab-release/
在linux下使用qiling框架执行exe文件
参考连接:https://www.cnblogs.com/Tu9oh0st/p/14012463.html
安装qiling框架
使用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
使用pyenv环境安装
Pyenv Installation with latest dev branch (recommended)
If you are using pyenv, run the command shown below.
python3 -m venv qilingenv
source qilingenv/bin/activate
git clone -b dev https://github.com/qilingframework/qiling.git
cd qiling && git submodule update --init --recursive
pip3 install .
使用docker安装
sudo docker pull qilingframework/qiling:latest
或用于 Qiling 框架 Docker 1.0 版本发布。
docker pull qilingframework/qiling:1.0
所需的DLL可以绑定到QILILE框架容器。假定DLL和HIVE文件位于/分析/Win/rootfs的子目录中
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
连接到正在运行的 Docker 容器。
docker exec -it qiling bash
Docker 容器端口可以通过 `-p` 开关进行发布。这对模拟如路由器中的 httpd 服务等用途非常有用
成功显示即是安装成功。
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
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)
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-challenge8
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)
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")
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()
参考:
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框架入门-QilingLab/
https://blog.csdn.net/Ga4ra/article/details/124412806
https://github.com/badmonkey7/qilinglab-solution
- 感谢你赐予我前进的力量