Qiling study note. It mainly talk about a program about some challenges. When you overcome it, you will master this skill.
Qiling框架学习 前几天在看雪公众号上看到这篇文章:Qiling框架分析实战 ,当时简单看了看开头,应该是IOT
漏洞利用的一个执行框架,感觉和我之后想干的很契合,所以打算学习学习。
Qiling
框架是基于 unicorn
的多架构平台模拟执行框架,能够在模拟执行的基础上提供统一的分析 API,可以进行插桩分析、快照、系统调用和API劫持等操作。Joansivion提供了能够针对Qiling
框架进行学习的程序 ,并提供了相应的writeup。他提供的 writeup 是 arm 架构的。本文也是对其提供的程序进行分析,但是提供的题解是x86_64的。
Joansivion提供的程序包括11个挑战,分为x86_64 版本与aarch64 版本。本文主要针对x86_64
,其挑战如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 Welcome to QilingLab. Here is the list of challenges: 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. 挑战1:将 1337 存储在指针 0x1337 处。 挑战2:使'uname'系统调用返回正确的值。 挑战3:使 '/dev/urandom' 和 'getrandom' '碰撞'。 挑战4:进入'禁止'循环。 挑战5:猜测对 rand() 的每次调用。 挑战6:避免无限循环。 挑战7:不要浪费时间等待'sleep'。 挑战8:解压结构体并写入目标地址。 挑战9:修复一些字符串操作以使iMpOsSiBlE成为现实。 挑战10:伪造'cmdline'行文件以返回正确的内容。 挑战11:绕过'CPUID/MIDR_EL1'检查。
使用qiling
运行一下x86_64
,显示错误:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 from qiling import * if __name__ == '__main__' : path = ["qilinglab-x86_64" ] rootfs = "./qiling/examples/rootfs/x8664_linux" ql = Qiling(path, rootfs) ql.run()
brk(inp = 0x0) = 0x55555575a000
:通过brk()
系统调用请求增加堆内存的结果,其中inp
参数为0,表示请求增加0字节的堆内存,返回值0x55555575a000
表示堆内存的起始地址。
uname(buf = 0x80000000d9b0) = 0x0
:通过uname()
系统调用获取系统信息的结果,其中buf
参数表示用于存储系统信息的缓冲区的地址,返回值0x0
表示操作成功。
access(path = 0x7ffff7df6082, mode = 0x0) = -0x1 (EPERM)
:通过access()
系统调用检查文件访问权限的结果,其中path
参数表示要检查的文件路径,mode
参数表示要检查的权限,返回值-0x1
表示操作失败,错误码为EPERM
,表示权限不允许。
openat(fd = 0xffffff9c, path = 0x7ffff7df6428, flags = 0x80000, mode = 0x0) = -0x2 (ENOENT)
:通过openat()
系统调用打开文件的结果,其中fd
参数表示文件描述符,path
参数表示要打开的文件路径,flags
参数表示打开文件的方式和行为,mode
参数表示文件的权限,返回值-0x2
表示操作失败,错误码为ENOENT
,表示文件不存在。
stat(path = 0x80000000d340, buf_ptr = 0x80000000d400) = -0x2 (ENOENT)
:通过stat()
系统调用获取文件状态的结果,其中path
参数表示要获取状态的文件路径,buf_ptr
参数表示用于存储状态信息的缓冲区的地址,返回值-0x2
表示操作失败,错误码为ENOENT
,表示文件不存在。该行出现了多次,表明程序尝试多次获取同一个文件的状态,但均返回了文件不存在的错误。
查看一下qilinglab-x86_64
的头部信息(-h
代表头部。)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 (base) wd@ubuntu:$ readelf -h qilinglab-x86_64 ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Shared object file) Machine: Advanced Micro Devices X86-64 Version: 0x1 Entry point address: 0xa80 Start of program headers: 64 (bytes into file) Start of section headers: 15840 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) Size of program headers: 56 (bytes) Number of program headers: 9 Size of section headers: 64 (bytes) Number of section headers: 29 Section header string table index: 28
从data
字段可以看出是小端序,从flags
字段可以看出未裁剪符号表。
查看一下文件类型,并显示符号链接的类型与其指向的文件。
1 2 (base) wd@ubuntu:~/Desktop/qiling$ file -L qilinglab-x86_64 qilinglab-x86_64: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, interpreter /lib64/ld-linux-x86-64.so.2, for GNU/Linux 3.2.0, BuildID[sha1]=76164e6b494c1af9d9f746e2dc7d3663cc23525c, not stripped
使用ida7.7
查看qilinglab-x86_64
。函数逻辑如下:
(1)循环初始化数组v13
所有元素为0,如果完成对应challenge1
就会改变v13
对应数组的值,再用checker
进行一次检查v13
数组元素是否为0
。最终,程序会输出完成的挑战数量,也就是数组v13
中为1的个数。
0x00 challenge1-修改内存地址
其中[rax]=v13[0]
,即我们想让[0x1337] = 0x539 = 1337
。在Qiling
中可以以多种方法编写字节序列:
1 2 3 4 5 6 # 返回4字节的大端序字节序列,其中包含整数值 0x12345678 的二进制表示 # ">I"参数指定了这个字节序列的格式,其中">"表示大端序,"<"表示小端序,"I"表示一个32位无符号整数 ql.pack(">I", 0x12345678) # 这个函数专门用于 16 位整数 ql.pack16(0x1234)
因此,可以编写challenage1-wp
,将[0x1337]
指向的内存为1337
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 from qiling import *def challenge1 (ql ): ql.mem.map (0x1000 , 0x1000 , info = '[challenge1]' ) ql.mem.write(0x1337 , ql.pack16(1337 )) if __name__ == '__main__' : path = ["qilinglab-x86_64" ] rootfs = "./qiling/examples/rootfs/x8664_linux" ql = Qiling(path, rootfs) challenge1(ql) ql.run()
0x01 challenge2-hook系统调用
通过分析,此函数的逻辑为:
(1)运行uname
函数,将返回结果保存到name
结构体中,如果成功获取的话返回值为0。
(2)判断name.sysname=="QilingOS"
,name.version=="ChallengeStart"
,就可以完成此挑战。
那么,我们要Hook
函数uname
,在返回之前改sysname
与version
。有4
种hook
方式:
(1)QL_INTERCEPT.EXIT
:在系统调用执行完成之后立即执行hook
函数。
(2)QL_INTERCEPT.ENTER
:在系统调用执行之前执行hook
函数。
(3)QL_INTERCEPT.EXIT_TREE
:在系统调用执行完成并返回后,执行完其他所有系统调用的hook
函数之后,再执行当前系统调用的hook
函数。
(4)QL_INTERCEPT.EXIT_ALL
:在系统调用执行完成并返回后,执行所有系统调用的hook
函数,并清除这些hook
函数。
补充:系统调用返回结构体型数据时,会将结构体的地址存放在寄存器rdi
中。uname
返回的结构体为:
1 2 3 4 5 6 7 8 struct utsname { char sysname[65]; char nodename[65]; char release[65]; char version[65]; char machine[65]; char domainname[65]; };
可以写脚本如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 from qiling import *from qiling.const import * def my_uname_on_exit_hook (ql, *args ): rdi = ql.arch.regs.rdi print (f"utsname address: {hex (rdi)} " ) ql.mem.write(rdi, b'QilingOS\x00' ) ql.mem.write(rdi + 65 * 3 , b'ChallengeStart\x00' ) def challenge2 (ql ): ql.os.set_syscall("uname" , my_uname_on_exit_hook, QL_INTERCEPT.EXIT) def challenge1 (ql ): ... if __name__ == '__main__' : path = ["./qiling/examples/rootfs/x8664_linux/qilinglab-x86_64" ] rootfs = "./qiling/examples/rootfs/x8664_linux" ql = Qiling(path, rootfs) challenge1(ql) challenge2(ql) ql.run()
0x02 challenge3-自定义文件
分析此函数:保证getrandom
函数拿到的数据v7
与文件/dev/urandom
拿到的数据buf
相同,且buf[i]!=v5
。其中,getrandom
是用系统调用获得随机数,此函数用法为:
1 2 3 4 5 6 7 8 9 10 11 ret = getrandom(ql, buf, buflen, flags) buf:指向缓冲区的指针,用于存储读取到的随机数据。 buflen:要从系统熵池读取的字节数。 flags: (1)0:如果系统熵池中没有足够的熵,getrandom 会阻塞直到有足够的熵可用。 (2)GRND_NONBLOCK(为 1):getrandom 在系统熵池中没有足够的熵时,会立即返回错误而不是阻塞。 (3)GRND_RANDOM(为 2):尝试从 /dev/random 获取随机数据,而不是从 /dev/urandom 获取。这个选项会导致 getrandom 的行为更加谨慎,可能会在熵不足时阻塞。 ret: (1)如果成功获取随机数据,getrandom 返回实际读取的字节数。 (2)如果出错,返回-1。
而/dev/urandom
是一个 Linux 系统中的特殊文件,它是一个伪随机数发生器设备文件,用于生成随机数。在qiling
中使用 ql.add_fs_mapper("/dev/urandom", "/dev/urandom")
将宿主机中的 /dev/urandom (后面的)
设备文件映射到qiling
虚拟机中的 /dev/urandom (前面的)
文件上,以便为虚拟机中的程序提供随机数服务。
wp
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 from qiling import *from qiling.const import * from qiling.os.mapper import QlFsMappedObjectclass FakeUrandom (QlFsMappedObject ): def read (self, size: int ) -> bytes : if size == 1 : return b"\x42" else : return b"\x41" * size def close (self ) -> int : return 0 def hook_getrandom (ql, buf, buflen, flags ): if buflen == 32 : data = b'\x41' * buflen ql.mem.write(buf, data) ql.os.set_syscall_return(buflen) else : ql.os.set_syscall_return(-1 ) def challenge3 (ql ): ql.add_fs_mapper(r'/dev/urandom' , FakeUrandom()) ql.os.set_syscall("getrandom" , hook_getrandom) def challenge2 (ql ): ... def challenge1 (ql ): ... if __name__ == '__main__' : path = ["./qiling/examples/rootfs/x8664_linux/qilinglab-x86_64" ] rootfs = "./qiling/examples/rootfs/x8664_linux" ql = Qiling(path, rootfs) challenge1(ql) challenge2(ql) challenge3(ql) ql.run()
0x03 challenge4-hook某地址
程序逻辑如下:初始时设置tmp1=tmp2=0
,比较tmp2<tmp1
是否成立,若成立则挑战成功。那么我们就要patch [rbp+tmp1]
为1。
可以编写wp
为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 import osfrom qiling import *def enter_forbidden_loop_hook (ql ): ql.arch.regs.eax = 1 def challenge4 (ql ): """ .text:0000555555554E40 mov eax, [rbp+tmp1] .text:0000555555554E43 cmp [rbp+tmp2], eax ; 若tmp2<tmp1则成功 <-- 在运行此命令前hook eax,使得eax = 1 .text:0000555555554E46 jl short loc_555555554E35 ; challenge4成功 """ base = ql.mem.get_lib_base(os.path.split(ql.path)[-1 ]) hook_addr = base + 0xE43 ql.hook_address(enter_forbidden_loop_hook, hook_addr) if __name__ == '__main__' : path = ["./qiling/examples/rootfs/x8664_linux/qilinglab-x86_64" ] rootfs = "./qiling/examples/rootfs/x8664_linux" ql = Qiling(path, rootfs) challenge4(ql) ql.run()
0x04 challenge5-hook某函数
函数逻辑如下:将v5[i]=0,v5[i+8]=rand(),0<=i<=4
。检验时,要求v5[i]=v5[i+8]
。思路是把rand()
函数hook
掉,感觉有点像challenge2
,不同的是,challenge2
是hook
的系统调用,而challenge5
是hook
的函数调用rand
,所以使用函数ql.os.set_api
。wp
如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 from qiling import *def my_rand_on_exit_hook (ql, *args ): ql.arch.regs.rax = 0 def challenge5 (ql ): ql.os.set_api('rand' , my_rand_on_exit_hook) if __name__ == '__main__' : path = ["./qiling/examples/rootfs/x8664_linux/qilinglab-x86_64" ] rootfs = "./qiling/examples/rootfs/x8664_linux" ql = Qiling(path, rootfs) challenge5(ql) ql.run()
需要注意的是,此时程序不会回显,因为要解决后面的题目。
0x05 challenge6-修改寄存器
程序一直循环,只有当var_5==0
时,才会跳出,我们打算将程序执行到0x0000555555554F12
时修改eax=0
。其wp
如下:
1 2 3 4 5 6 def hook_rax (ql ): ql.arch.regs.rax = 0 def challenge6 (ql ): base = ql.mem.get_lib_base(os.path.split(ql.path)[-1 ]) hook_addr = base + 0xF16 ql.hook_address(hook_rax, hook_addr)
0x06 challenge7-hook函数调用
程序一直sleep
,很难返回,我们要hook sleep
函数,如下所示:
1 2 3 4 def hook_sleep (ql ): return 0 def challenge7 (ql ): ql.os.set_api('sleep' , hook_sleep)
0x07 challenge8-修改结构体的值
这是一个结构体,其整理后的内容如下:
1 2 3 4 | 8 bytes | 30-byte memory, save "Random data" | | 4 bytes | 1337 | | 4 bytes | 1039980266 | | 8 bytes | a1 |
结构体定义如下:
1 2 3 4 5 6 typedef struct { char *string_ptr; // 8 字节的指针,指向字符串 "Random data" 所在的内存 uint32_t value1; // 4 字节的整数,值为 1337 uint32_t value2; // 4 字节的整数,值为 1039980266 int64_t a1; // 8 字节的整数,值为传入的参数 a1 } CustomStruct;
我们需要修改*a1=1
,那么wp
可以这么写:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 import struct def challenge8_hook (ql ): MAGIC = 0x3DFCD6EA00000539 magic_addrs = ql.mem.search(ql.pack64(MAGIC)) for magic_addr in magic_addrs: candidate_heap_struct_addr = magic_addr - 8 candidate_heap_struct = ql.mem.read(candidate_heap_struct_addr, 24 ) string_addr, _ , check_addr = struct.unpack('QQQ' , candidate_heap_struct) if ql.mem.string(string_addr) == "Random data" : ql.mem.write(check_addr, b"\x01" ) break def challenge8 (ql ): base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1 ]) end_of_challenge8 = base_addr + 0xFB5 ql.hook_address(challenge8_hook, end_of_challenge8)
0x08 challenge9-hook函数调用2
把aBcdeFghiJKlMnopqRstuVWxYz
变成小写,并检查src
与dest
是否相等,若相等则挑战成功。目测应该是要hook strcmp
函数。wp
如下,其hook
了tolower
函数,让它什么也不做:
1 2 3 4 def hook_tolower (ql ): return 0 def challenge9 (ql ): ql.os.set_api('tolower' , hook_tolower)
0x09 challenge10-自定义文件对象
读取/proc/self/cmdline
文件,并比较读取的字符串是否为"qilinglab"
。
注:/proc/self/cmdline
是一个在Linux
系统中的特殊文件,它提供了当前进程(即访问/proc/self/cmdline
的进程)的命令行参数信息。假如我们使用命令./my_program arg1 arg2 arg3
启动一个程序时,读取/proc/self/cmdline
的过程在 ./my_program
中,/proc/self/cmdline
文件的内容将是:./my_program\0arg1\0arg2\0arg3\0
。
按照challenge3
,可以写wp
:
1 2 3 4 5 6 7 8 class Fake_cmdline (QlFsMappedObject ): def read (self, expected_len ): return b'qilinglab' def close (self ): return 0 def challenge10 (ql ): ql.add_fs_mapper('/proc/self/cmdline' , Fake_cmdline())
博客中此wp
在运行后并没有解题成功,博主说是在hook /proc/self/cmdline
这里存在问题,所以他写了个脚本单独调试以下cmdline
读取程序:
c
程序:用来读取cmdline
,文件名:test_cmdline
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 #include <stdio.h> #include <stdlib.h> int main () { FILE *file; char ch; file = fopen ("/proc/self/cmdline" , "r" ); if (file == NULL ) { printf ("无法打开/proc/self/cmdline文件。\n" ); exit (EXIT_FAILURE); } printf ("读取/proc/self/cmdline:\n" ); while ((ch = fgetc (file)) != EOF) { if (ch == '\0' ) { putchar ('\n' ); } else { putchar (ch); } } fclose (file); return 0 ; }
结果博主发现:用c
语言读取程序存在问题。但是我没有看出来有什么问题(: 最终经过代码审计(好长的步骤),发现qiling-1.4.3
可以正常运行wp
。
0x10 challenge11-绕过CPUID校验
要让if
的判断顺利通过,if ( __PAIR64__(_RBX, _RCX) == 0x696C6951614C676ELL && (_DWORD)_RDX == 538976354 )
的含义为:
(1)rbx
和rcx
两个32位寄存器组成的64位值等于0x696C6951614C676ELL
。
(2)RDX
寄存器的低32位等于538976354
。
因此我们跳过cpuid
指令,并修改寄存器即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 def hook_cpuid (ql, address, size ): if ql.mem.read(address, size) == b'\x0F\xA2' : regs = ql.arch.regs regs.ebx = 0x696C6951 regs.ecx = 0x614C676E regs.edx = 0x20202062 regs.rip += 2 def challenge11 (ql ): begin, end = 0 , 0 for info in ql.mem.map_info: print (info) if info[2 ] == 5 and 'qilinglab-x86_64' in info[3 ]: begin, end = info[:2 ] print (f"{begin} -> {end} " ) ql.hook_code(hook_cpuid, begin=begin, end=end)