Qiling是一款功能强大的高级代码模拟框架
很早就知道了qiling框架,一直想学但一直都忙于别的事情,这段时间打算开始学习qiling框架,记录一下学习的历程
0x1.运行程序
编写一个简单的hello world程序,交叉编译为mipsel架构的,使用qiling来运行,脚本如下
1 | import sys |
运行结果如下
执行效果类似于strace
看到Qiling这个类
参数非常多,我们目前先看到上面的脚本用到的三个参数,剩下的参数啥时候用到了啥时候再分析。
1 | ql=Qiling(["./hello"],"/usr/mipsel-linux-gnu",verbose=QL_VERBOSE.DEFAULT) |
[]内部的是要执行的程序以及执行所需的参数,我们将程序修改为如下
1 |
|
将脚本修改为
1 | import sys |
和正常在终端运行程序一样,将程序名和参数都写入[]即可
第二个参数,"/usr/mipsel-linux-gnu",库文件的路径
第三个参数,输出的详细程度,在我的脚本中使用的是QL_VERBOSE.DEAFAULT,还有其他的参数可供选择
1 | class QL_VERBOSE(IntEnum): |
每种的效果怎样我就不说了,师傅们可以自己尝试。
这就是第一个qiling脚本,学习如何使用qiling运行不同架构的程序。
0x2 使用qiling进行调试
只需要在ql.run()之前添加一句ql.debugger=True即可,这样子默认就会监听9999端口,官方给的方法有这些
1 | You can also customize address & port or type of debugging server |
效果如下
感觉还不错
0x3.使用qiling hook程序
hook可以玩的很花,这里就按照官方文档的顺序来讲解,不过只学习几个常用的方法
0x1.ql.hook_address()
使用ql.hook_address()来hook一个特定的地址
目标是hook掉0x4004fd
1 | from qiling.const import QL_VERBOSE |
运行结果如下
在0x4004fd处执行了我们的hook函数
0x2. ql.hook_code()
ql.hook_code()
hooking every instruction with self defined function
可以hook每一条指令
1 | from qiling.const import QL_VERBOSE |
官方文档中给的例子是打印每一条汇编指令,hook函数需要给三个参数,ql对象,指令的地址,指令的长度
这是第一个printf_asm函数
0x4.qilinglab-x86
这是一个学习qiling框架的小练习,一共11个挑战,方便我们快速入门qiling,以11个小挑战,Qiling Framework 入门上手跟练和Qiling Labs为教程跟随练习
前者的架构为x86_64架构,后者为aarch64,我这里使用x86_64的程序
一共有如下11个挑战
1 | Challenge 1: Store 1337 at pointer 0x1337 |
IDA反编译看看
0x1.challenge1

检查0x1337这个地址里面的值是不是1337,因此我们需要往0x1337处写入1337
映射一块内存
1 | ql.mem.map(0x1000,0x1000,info="challenge1") |
地址从0x1000开始,长度为0x1000,起始地址和长度有如下要求
1 | Address: |
size需要4k对齐
challenge1的代码如下
1 | def challenge1(ql:Qiling): |
0x2.challenge2
uname这个系统调用用来获得操作系统的一些信息的,其参数name的类型是struct utsname类型,如下
1 | struct utsname |
challenge2要使sysname为QilingOS,version为ChallengeStart,如何hook系统调用,在qiling中使用ql.set_syscall,原型如下
1 | def set_syscall(self, target_syscall, intercept_function, intercept = None): |
target_syscall是目标系统调用,intercept_function是我们自定义的系统调用,intercept定义在什么时候拦截系统调用
首先编写我们自己的系统调用,uname的参数是name结构体的地址,传参为rdi,调用结束后将值写入指针所指的内存中。我们可以在调用结束后,也就是即将退出系统调用的时候拦截uname,修改rdi指向内存的数据,代码如下
1 | def hook_uname_on_exit(ql:Qiling,*args,**kw): |
这个是我们自定义的系统调用,首先获取rdi的值,即name结构体的指针,然后往指针指向的内存中写入数据,像内存中写入数据使用ql.mem.write
根据上面的utsname结构体的定义,sysname是第一个结构体成员,version是第四个每一个成员的长度为65字节
1 | def challenge2(ql:Qiling): |
使用QL_INTERCEPT.EXIT需要引入头文件from qiling.const import *
0x3.challenge3

从/dev/urandom中读出32个字节的随机数要与用getrandom函数获取的32个随机数全部相同,另外再从/dev/urandom中读出1字节,这1字节不能与上面的32个字节相同。
challenge3涉及到了另一个知识,劫持文件系统。在qiling中,涉及到伪造文件系统的类都需要继承自QlFsMappedObject这个类,此挑战我们可以这样伪造文件系统
1 | class Fake_urandom(QlFsMappedObject): |
伪造的文件系统至少需要read和close两个方法,在这个fake_urandom类中,如果读取的长度是1,就返回一个数字1,如果长度不为1,则返回size个空字符。这两种情况对应着challenge3中从/dev/urandom中读取32个字节和1个字节的情况。
再看到getrandom,这也是个系统调用,原型如下
1 | ssize_t getrandom(void * buf,size_t buflen,unsigned int标志); |
往buf中写入buflen个随机数。
成功后,getrandom()返回复制到缓冲区buf的字节数。如果在标志中指定了GRND_RANDOM,并且随机源中没有足够的熵,或者系统调用被信号中断,则该数目可能小于通过buflen请求的字节
我们hook掉getrandom这个系统调用,只需在我们的hook函数中往buf中写入size*’\x00’即可
1 | def getrandom_hook(ql:Qiling,buf,size,flags,*args,**kw): |
0x4.challenge4
将a和b的值都设置为0,然后比较a和b的值,如果a小于b就将challenge4要检查的值设为1,否则直接退出。
由于a等于b,所以实际上是不能进入到loc_E35的,我们要做的就是使jl这个指令成立。由于a=0,所以我们只需要在程序运行到0xE43时将eax修改为1,就能满足jl,进入到loc_E35
这里需要用的是ql.hook_address
1 | def hook_address(self, callback, address, user_data=None): |
callback为执行到address时执行的函数
于是我们这样写
1 | def get_base(ql:Qiling): |
关于这个get_base函数,我用它来获取程序运行基地址,实际上,在qiling中有ql.mem.get_lib_base(ql.path)用来获取基地址的,不过我用起来似乎出了点问题,返回值为-1,就利用ql.mem.map_info写了一个
ql.mem.map_info实际上就是下面这个
进程的内存分布,每一行都是一个数组,长度为5,数组下标从0到4分别对应着Start,End,Perm,Label,Image的值。
0x5.challenge5

在一个循环中将v5这个数组的前面一截赋值为0,后面一截赋值为随机数,然后检查这两段的值是否一样。
很明显,我们需要hook掉rand函数,使其返回0
rand函数的返回值是放在eax的,因此我们在hook函数中将eax设为0,由于rand函数不是系统调用,我们这里用到ql.set_api
1 | def hook_rand(ql:Qiling): |
比较简单的一个challenge
0x6.challenge6
将0和1分别作为初值赋给i和j,然后将判断j是否为0,如果为0即通过挑战
显然,正常走流程是直接结束了。这个挑战和挑战4是类似的,依葫芦画瓢即可,在jnz那条指令处进行hook,将rax设置为0即可
1 | def infinite_loop_bypass_hook(ql:Qiling): |
0x7.challenge7

有一个sleep函数,沉睡时间无限长,很显然我们需要hook掉sleep函数
第一个参数传参为rdi,我们可以修改sleep的参数为0就能退出sleep
还可以在sleep函数中直接return
1 | def hook_sleep(ql:Qiling): |
另外,sleep底层调用的是nanosleep函数,也可以对nanosleep进行hook
0x8.challenge8

这里借用了JOANSIVION的结构体
1 | struct random_struct { |
challenge8最后会将要检查的位置的地址赋给check_addr,我们要做的就是在执行这一步之后将check_addr指向的位置赋值为1.我们在0xfb5处进行hook
要做到这点我们首先要获取v2的地址
在IDA中我们可以看到各个变量在栈中的偏移,v2这个结构体的地址存在rbp-8处,获取到地址之后,然后根据结构体偏移读取check_addr的值,然后往其中写1即可
1 | def hook_struct(ql:Qiling): |
0x9.challenge9

将这一串字符串转为小写后,然后和原字符串比较,如果相同则通过检查
两个函数,tolower和strcmp,可以让tolower直接返回,或者让strcmp返回0
1 | def hook_tolower(ql:Qiling): |
0xA.challenge10

从/proc/self/cmdline中读取当前的命令,如果是qilinglab的话就通过检查
这个挑战和challenge2是一样的,劫持文件系统,使其被读取的时候返回qilinglab即可
1 | class Fake_cmdline(QlFsMappedObject): |
0xB.challenge11

这个challenge使用cpuid来获取cpu的一些信息,然后检查RBX,RCX,RDX的值是否为特定的值,是的话则通过检查。伪代码中的__PAIR64__应该是IDA自定义的一个,__PAIR64__(_RBX,_RCX) == 0x696C6951614C676ELL实际上检查RBX==0x696C6951,RCX==0X614C676E。
这题需要hook指令,ql.hook_code,hook掉cpuid,不让它正常执行
1 | def hook_cpuid(ql:Qiling,address,size): |
另外,除了用hook_code的方式,还可以用hook_address
在0x1195位置,会把esi,ecx,eax的值赋值到内存中,然后进行if比较,从这里下手,我们在1195处进行hook,设置好esi,ecx和eax的值为正确的值就好
1 | def hook_cpuid(ql:Qiling): |
0x5 qilinglab-aarch64
0x1.challenge1
0x6.使用qiling模拟运行路由器固件
0x1.DIR816A2
http后台程序为/bin/goahead
首先用以下代码尝试运行goahead
1 | from qiling.const import * |
得到如下输出
缺少/dev/nvram文件,于是我们手动在rootfs下面创建这个文件,继续运行
在IDA中对这个字符进行交叉引用,查找到报错代码

缺少/var/run/goahead.pid文件,继续手动创建,接着运行,得到如下报错
在IDA中定位
问题出在inet_addr转换的时候,看到汇编
比较$s1和$v0寄存器的值是否相等,$v0的值是-1,而$s1的值来源于inet_addr的返回值,因此我们需要在这里进行hook,使其不相等。
采用如下代码进行hook
1 | def hook_inet_addr(ql:Qiling): |
再次运行
得到这样的输出,可以直接忽略
netstat查看端口
在浏览器中输入这个IP:port
成功运行
0x2.DAP1665
这个是个无线扩展器,所有的http处理功能都集成在一个cgibin文件中
尝试直接运行
1 | from qiling.const import * |
会有如下输出
1 | CGI.BIN, unknown command cgibin |
调试过cgibin的师傅应该知道,cgibin会根据不同的需求进入到不同分支
我们运行失败正是因为没有指定进入的分支,如何处理呢?
前面在qilinglab中,我们可以设置寄存器的值,同样的我们可以设置pc的值,使程序流向我们想要的分支。在这里我选择进入到pigwidgeon.cgi(因为需要的参数少一点),我们在main函数入口处进行hook,设置pc寄存器为pigwidgeon分支的入口地址,程序如下
1 | from qiling.const import * |
成功运行
进入到pigwidgeon查看
首先会取得请求方式,而且只能为POST,我们继续设置环境变量
1 | env_vars={ |
回显发生变化
无法解析HTTP请求,因为我们除了请求方式外什么都没有设置,在查看其他获取环境变量的函数
这里获取了另外两个环境变量,继续按要求设置
1 | env_vars={ |

可以得到不同的回显。这里只是做一个示例,因为环境变量的设置需要仔细审计完代码才能按照要求设置,就不细看了。
另外提一嘴,qiling的debugger用起来体验并不好,无论是pwdbg还是gef远程连上去后都看不到寄存器,要调试的话还是得用qemu来
0x3.Tenda AC15
直接运行httpd的话会在check_network处卡住
一路追踪check_network,找到了最终调用的函数
get_eth_name传入的参数是0,所以check_network是要获取br0的ip,而我的虚拟机中并没有br0这个网卡











