初探Qiling framework

admin 2025年1月6日22:01:56评论14 views字数 22044阅读73分28秒阅读模式

做完这些lab,对qiling的作用,这个framework的理解,更加的深,希望看到这里的读者可以自己去做一遍,不要纯看他人的wp,因为每道题都有着很多种解法,但是目标都是成功的hook或Hijack,Qiling还有着一些fuzz or 仿真的 example,我都会去做一遍的!

What is Qiling


Qiling是一个先进的二进制仿真框架,具有以下特点:

Qiling这个框架对于模拟运行二进制程序时的 hook 非常方便

◆模拟多平台:Windows、MacOS、Linux、Android、BSD、UEFI、DOS、MBR、以太坊虚拟机

◆模拟多架构:8086、X86、X86_64、ARM、ARM64、MIPS、RISCV、PowerPC

◆内置调试器,具有逆向调试功能

◆提供深入的内存、寄存器、操作系统级和文件系统级API

◆细粒度检测:允许在各个级别进行挂钩(指令/基本块/内存访问/异常/系统调用/IO/等)

◆支持跨架构和平台调试能力

◆真正的Python框架,可以轻松地在其上构建定制的安全分析工具

◆等等

qiling framework的github项目上还有几个示例demo,这里注意到了通过Qiling框架仿真模拟并对二进制程序进行hook可以更加方便的fuzz。

Lab


challenge1

要求在0x1337地址上写入0x1337:

Method Description
map Map a memory region at a certain location so it become available for access
unmap Reclaim a mapped memory region
unmap_all Reclaim all mapped memory regions
map_anywhere Map a memory region in an unspecified location
protect Modify access protection bits of a mapped region (rwx)
find_free_space Find an available memory region
is_available Query whether a memory region is available
is_mapped Query whether a memory region is mapped

Qiling给出了这些Managing memory的方法。

内存在被访问之前必须被映射。map方法将连续的内存区域绑定到指定位置,并设置其访问保护位。可以提供字符串标签以便在映射信息表上轻松识别。

ql.mem.map(addr: int, size: int, perms: int = UC_PROT_ALL, info: Optional[str] = None) -> None

参数:-addr- 请求的映射基地址,应该在页面粒度上;-size- 映射大小(以字节为单位),必须是页面大小;-perms- 保护位图的乘积,定义此内存范围是否可读、可写和/或可执行(可选);-info- 将字符串标签设置为映射范围以方便识别(可选)

主要也是关注前两个参数,这里显然是执行ql.mem.map(0x1337// 4096 * 4096 , 0x1000);

ql.mem.unmap(addr: int, size: int) -> None:

参数:-addr- 要取消映射的区域基地址 -size- 区域大小(以字节为单位)

如果请求的内存范围未完全映射,则引发:QlMemoryMappedError

address = ql.mem.search(b"xFFxFExFDxFCxFBxFA", begin= 0x1000, end= 0x2000)

从部分内存范围中搜索字符串,begin和end参数均是可选的,去除则是整个内存范围。

ql.mem.read(address, size)

从内存中读取:

ql.mem.write(address, data)

写入内存:

def challenge1(ql):
    ql.mem.map(0x1337//4096*4096 , 0x1000)
    ql.mem.write(0x1337, b"x39x05")

challenge2

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[24]; // [rsp+1B0h] [rbp-30h] BYREF
  unsigned __int64 v9; // [rsp+1C8h] [rbp-18h]

  v9 = __readfsqword(0x28u);
  if ( uname(&name) )
  {
    perror("uname");
  }
  else
  {
    strcpy(s, "QilingOS");
    s[9] = 0;
    strcpy(v8, "ChallengeStart");
    v8[15] = 0;
    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返回系统信息时的sysname == "QilingOS" and version == "ChallengeStart"

需要通过劫持结构体

Hijack

POSIX 系统调用可以被挂钩以允许用户修改其参数改变返回值完全替换其功能。系统调用可以通过其名称或编号进行挂钩,并在一个或多个阶段进行拦截:- QL_INTERCEPT.CALL -:当指定的系统调用即将被调用时,可用于完全替换系统调用功能;- QL_INTERCEPT.ENTER -:在进入系统调用之前;可用于篡改系统调用参数值- QL_INTERCEPT.EXIT -:退出系统调用后,可能被用来篡改返回值。

from qiling import Qiling
from qiling.const import QL_INTERCEPT

# customized system calls always use the same arguments list as the original
# ones, but with a Qiling instance on front. The Qiling instance may be used
# to interact with various subsystems, such as the memory or registers
def my_syscall_write(ql: Qiling, fd: int, buf: int, count: int) -> int:
    try:
        # read data from emulated memory
        data = ql.mem.read(buf, count)

        # select the emulated file object that corresponds to the requested
        # file descriptor
        fobj = ql.os.fd[fd]

        # write the data into the file object, if it supports write operations
        if hasattr(fobj, 'write'):
            fobj.write(data)
    except:
        ret = -1
    else:
        ret = count

    ql.log.info(f'my_syscall_write({fd}, {buf:#x}, {count}) = {ret}')

    # return a value to the caller
    return ret

if __name__ == "__main__":
    ql = Qiling([r'rootfs/arm_linux/bin/arm_hello'], r'rootfs/arm_linux')

    # the following call to 'set_syscall' sets 'my_syscall_write' to execute whenever
    # the 'write' system call is about to be called. that practically replaces the
    # existing implementation with the one in 'my_syscall_write'.
    ql.os.set_syscall('write', my_syscall_write, QL_INTERCEPT.CALL)

    # note that system calls may be referred to either by their name or number.
    # an equivalent alternative that replaces the write syscall by refering its number:
    #
    #ql.os.set_syscall(4, my_syscall_write)

    ql.run()

因此要通过ql.os.set_syscall中的QL_INTERCEPT.EXIT对返回值进行篡改。

def my_uname_ret(ql ,*args): 
    '''
    struct utsname
    {
        char sysname[65];
        char nodename[65];
        char release[65];
        char version[65];
        char machine[65];
        char domainname[65];
    };
    '''
    rdi_value=ql.arch.regs.read("rdi")
    ql.mem.write(rdi_value,b'QilingOSx00')
    ql.mem.write(rdi_value+65*3,b'ChallengeStartx00')
    return 0

def challenge2(ql):
    ql.os.set_syscall("uname",my_uname_ret,QL_INTERCEPT.EXIT)

这里有个要注意的点,my_uname_ret的参数要能接受三个参数,不然会报错。

这里看到qiling的一个example:

onexit_hook(self.ql, *self.get_syscall_args())

Qiling都是通过一些函数,来进行hook的。

Qiling的这些hook,都是一些在程序的hook,并不像正常的程序的输入输出,例如这里在退出的地方执行这个oxexit_hook函数(个人理解)

challenge3

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 v7[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)v7, 32LL, 1LL);
  v2 = 0;
  for ( i = 0; i <= 31; ++i )
  {
    if ( buf[i] == v7[i] && buf[i] != v5 )
      ++v2;
  }
  if ( v2 == ' ' )
    *a1 = 1;
  return __readfsqword(0x28u) ^ v8;
}

这里的第一个想法就是hook掉getrandom,控制其函数体,然后将虚拟路径/dev/urandom映射到文件对象下。

以下示例将虚拟路径/dev/urandom映射到托管系统上现有的/dev/urandom文件。当模拟程序访问/dev/random时,将访问映射文件。

from qiling import Qiling

if __name__ == "__main__":
    ql = Qiling([r'rootfs/x86_linux/bin/x86_fetch_urandom'], r'rootfs/x86_linux')

    ql.add_fs_mapper(r'/dev/urandom', r'/dev/urandom')
    ql.run()

以下示例将虚拟路径/dev/random映射到用户定义的文件对象,以允许对交互进行更细粒度的控制。请注意,映射对象扩展了QlFsMappedObject

from qiling import Qiling
from qiling.os.mapper import QlFsMappedObject

class FakeUrandom(QlFsMappedObject):

    def read(self, size: int) -> bytes:
        # return a constant value upon reading
        return b"x04"

    def fstat(self) -> int:
        # return -1 to let syscall fstat ignore it
        return -1

    def close(self) -> int:
        return 0

if __name__ == "__main__":
    ql = Qiling([r'rootfs/x86_linux/bin/x86_fetch_urandom'], r'rootfs/x86_linux')

    ql.add_fs_mapper(r'/dev/urandom', FakeUrandom())
    ql.run()

注意到:

class FakeUrandom(QlFsMappedObject):

    def read(self, size: int) -> bytes:
        # return a constant value upon reading
        return b"x04"

    def fstat(self) -> int:
        # return -1 to let syscall fstat ignore it
        return -1

    def close(self) -> int:
        return 0

这里其实是一个类,然后定义了一些函数,用self来代替/dev/urandom这个对象。

class FakeUrandom(QlFsMappedObject):

    def read(self, size=int) -> bytes:
        if size==0x20:
            return b"x02"*32
        # return a constant value upon reading
        return b"x01"

    def fstat(self) -> int:
        # return -1 to let syscall fstat ignore it
        return -1

    def close(self) -> int:
        return 0

def my_getrandom_func(ql, buf, count:int, flag:int) ->int:
    ql.mem.write(buf,b'x02'*count)
    return count

def challenge3(ql):
    ql.add_fs_mapper(r'/dev/urandom', FakeUrandom())
    ql.os.set_syscall("getrandom",my_getrandom_func,QL_INTERCEPT.CALL)

challenge4

.text:0000000000000E1D                               ; __int64 challenge4()
.text:0000000000000E1D                               public challenge4
.text:0000000000000E1D                               challenge4 proc near                    ; CODE XREF: start+18F↓p
.text:0000000000000E1D
.text:0000000000000E1D                               var_18= qword ptr -18h
.text:0000000000000E1D                               var_8= dword ptr -8
.text:0000000000000E1D                               var_4= dword ptr -4
.text:0000000000000E1D
.text:0000000000000E1D                               ; __unwind {
.text:0000000000000E1D 55                            push    rbp
.text:0000000000000E1E 48 89 E5                      mov     rbp, rsp
.text:0000000000000E21 48 89 7D E8                   mov     [rbp+var_18], rdi
.text:0000000000000E25 C7 45 F8 00 00 00 00          mov     [rbp+var_8], 0
.text:0000000000000E2C C7 45 FC 00 00 00 00          mov     [rbp+var_4], 0
.text:0000000000000E33 EB 0B                         jmp     short loc_E40
.text:0000000000000E33
.text:0000000000000E35                               ; ---------------------------------------------------------------------------
.text:0000000000000E35
.text:0000000000000E35                               loc_E35:                                ; CODE XREF: challenge4+29↓j
.text:0000000000000E35 48 8B 45 E8                   mov     rax, [rbp+var_18]
.text:0000000000000E39 C6 00 01                      mov     byte ptr [rax], 1
.text:0000000000000E3C 83 45 FC 01                   add     [rbp+var_4], 1
.text:0000000000000E3C
.text:0000000000000E40
.text:0000000000000E40                               loc_E40:                                ; CODE XREF: challenge4+16↑j
.text:0000000000000E40 8B 45 F8                      mov     eax, [rbp+var_8]
.text:0000000000000E43 39 45 FC                      cmp     [rbp+var_4], eax
.text:0000000000000E46 7C ED                         jl      short loc_E35
.text:0000000000000E46
.text:0000000000000E48 90                            nop
.text:0000000000000E49 5D                            pop     rbp
.text:0000000000000E4A C3                            retn
.text:0000000000000E4A                               ; } // starts at E1D
.text:0000000000000E4A
.text:0000000000000E4A                               challenge4 endp

这里会是一个循环,我们想要的是使参数为真,而loc_E35块可以满足我们的要求,但是jl short loc_E35是显然无法满足的,因为-4和-8的位置都被置为0了是相等的,而jl是小于跳转,因此要想办法跳转到loc_E35块上。

读寄存器

◆从字符串“eax”读取

ql.arch.regs.read("EAX")

◆从 Unicorn Engine const 读取

ql.arch.regs.read(UC_X86_REG_EAX)

◆读eax

eax = ql.arch.regs.eax

◆将 0xFF 写入“eax”

ql.arch.regs.write("EAX", 0xFF)

◆通过 Unicorn Engine const 将 0xFF 写入 eax

ql.arch.regs.write(UC_X86_REG_EAX, 0xFF)

◆将 0xFF 写入 eax

ql.arch.regs.eax =  0xFF

还有一些跨架构寄存器的获取方法:

ql.arch.regs.arch_pc
ql.arch.regs.arch_sp  #这仅适用于 PC 和 SP。
ql.arch.regs.arch_pc = 0xFF
ql.arch.regs.arch_sp = 0xFF  #从当前架构上的 PC/SP 读取,由 ql.arch.type 定义

◆获取当前arch寄存器表列表

ql.arch.regs.register_mapping()

◆在 64 位环境中,这将返回 64

ql.arch.reg_bits("rax")

◆在 64 位环境中,这将返回 32

ql.arch.reg_bits("eax")

Hook

挂钩具体地址。执行指定地址时将调用已注册的回调。

ql.hook_address(回调:可调用,地址:int)
from qiling import Qiling

    def stop(ql: Qiling) -> None:
        ql.log.info('killer switch found, stopping')
        ql.emu_stop()

    ql = Qiling([r'examples/rootfs/x86_windows/bin/wannacry.bin'], r'examples/rootfs/x86_windows')

    # have 'stop' called when execution reaches 0x40819a
    ql.hook_address(stop, 0x40819a)

    ql.run()

挂钩所有说明。注册的回调将在每个汇编指令执行之前调用:

ql.hook_code(回调:可调用, user_data :任何=无)
from capstone import Cs
from qiling import Qiling
from qiling.const import QL_VERBOSE

def simple_diassembler(ql: Qiling, address: int, size: int, md: Cs) -> None:
    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', verbose=QL_VERBOSE.DEBUG)

    # have 'simple_disassembler' called on each instruction, passing a Capstone disassembler instance bound to
    # the underlying architecture as an optional argument
    ql.hook_code(simple_diassembler, user_data=ql.arch.disassembler)

    ql.run()

还有一些hook,例如挂钩一段代码、挂钩中断号以调用自定义函数、拦截特定类型的指令等等。

我的想法是在:

.text:0000000000000E43 39 45 FC                      cmp     [rbp+var_4], eax

这个地址执行前,在eax里写入1,这样就使得jl条件达成,就成功跳转到成功片段。

def write_eax_1(ql):
    ql.arch.regs.write("EAX",1)

def challenge4(ql):
    base_addr = ql.mem.get_lib_base(ql.path)
    ql.hook_address(write_eax_1,base_addr+0x0E43)

challenge5

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即可。

这里不是系统调用的劫持,而是劫持libc函数,这里看个例子。

与系统调用一样,POSIX libc 函数可以以类似的方式挂钩,从而允许用户控制其功能。

from qiling import Qiling
from qiling.const import QL_INTERCEPT
from qiling.os.const import STRING

# customized POSIX libc methods accept a single argument that refers to the active
# Qiling instance. The Qiling instance may be used to interact with various subsystems,
# such as the memory or registers. The customized method may or may not return a value
def my_puts(ql: Qiling):
    # Qiling offers a few conviniency methods that abstract away the access to the call
    # parameters. specifying the arguments names and types woud allow Qiling to retrieve
    # their values and parse them accordingly.
    #
    # the following call lists a single argument named 's', whose type is 'STRING'.
    # a dictionary will be created having the key 's' mapped to the null-terminated
    # string read from the memory address pointed by the first argument.
    params = ql.os.resolve_fcall_params({'s': STRING})

    s = params['s']
    ql.log.info(f'my_puts: got "{s}" as an argument')

    # emulate puts functionality
    print(s)

    return len(s)

if __name__ == "__main__":
    ql = Qiling([r'rootfs/x8664_linux/bin/x8664_hello'], r'rootfs/x8664_linux')

    ql.os.set_api('puts', my_puts, QL_INTERCEPT.CALL)
    ql.run()
def rand_rets(ql ,*args):
    ql.arch.regs.write("rax",0)

def challenge5(ql):
    ql.os.set_api('rand', rand_rets)
    return

challenge6

.text:0000000000000EF6                               public challenge6
.text:0000000000000EF6                               challenge6 proc near                    ; CODE XREF: start+1D7↓p
.text:0000000000000EF6
.text:0000000000000EF6                               var_18= qword ptr -18h
.text:0000000000000EF6                               var_5= byte ptr -5
.text:0000000000000EF6                               var_4= dword ptr -4
.text:0000000000000EF6
.text:0000000000000EF6                               ; __unwind {
.text:0000000000000EF6 55                            push    rbp
.text:0000000000000EF7 48 89 E5                      mov     rbp, rsp
.text:0000000000000EFA 48 89 7D E8                   mov     [rbp+var_18], rdi
.text:0000000000000EFE C7 45 FC 00 00 00 00          mov     [rbp+var_4], 0
.text:0000000000000F05 C6 45 FB 01                   mov     [rbp+var_5], 1
.text:0000000000000F09 EB 07                         jmp     short loc_F12                   ; 零扩展
.text:0000000000000F09
.text:0000000000000F0B                               ; ---------------------------------------------------------------------------
.text:0000000000000F0B
.text:0000000000000F0B                               loc_F0B:                                ; CODE XREF: challenge6+22↓j
.text:0000000000000F0B C7 45 FC 01 00 00 00          mov     [rbp+var_4], 1
.text:0000000000000F0B
.text:0000000000000F12
.text:0000000000000F12                               loc_F12:                                ; CODE XREF: challenge6+13↑j
.text:0000000000000F12 0F B6 45 FB                   movzx   eax, [rbp+var_5]                ; 零扩展
.text:0000000000000F16 84 C0                         test    al, al
.text:0000000000000F18 75 F1                         jnz     short loc_F0B
.text:0000000000000F18
.text:0000000000000F1A 48 8B 45 E8                   mov     rax, [rbp+var_18]
.text:0000000000000F1E C6 00 01                      mov     byte ptr [rax], 1
.text:0000000000000F21 90                            nop
.text:0000000000000F22 5D                            pop     rbp
.text:0000000000000F23 C3                            retn
.text:0000000000000F23                               ; } // starts at EF6
.text:0000000000000F23
.text:0000000000000F23                               challenge6 endp

这里会有一个无限循环,movzx eax, [rbp+var_5]先eax零扩展赋值为1,之后test al, al对al进行逻辑与的运算,结果存入ZF中,而jnz是ZF不为0则跳转,显然会有跳转,之后便是无限循环,我的想法 便是hook rax为0即可。

def write_rax_0(ql):
    ql.arch.regs.write("rax",0)

def challenge6(ql):
    base_addr = ql.mem.get_lib_base(ql.path)
    if base_addr is None:
        raise ValueError("base_addr is not set correctly")
    ql.hook_address(write_rax_0,base_addr+0xF16)
def write_rax_0(ql):
    ql.arch.regs.write("rax",0)

def challenge6(ql):
    base_addr = ql.mem.get_lib_base(ql.path)
    if base_addr is None:
        raise ValueError("base_addr is not set correctly")
    ql.hook_address(write_rax_0,base_addr+0xF16)

challenge7

.text:0000000000000F24                               public challenge7
.text:0000000000000F24                               challenge7 proc near                    ; CODE XREF: start+1FB↓p
.text:0000000000000F24
.text:0000000000000F24                               var_8= qword ptr -8
.text:0000000000000F24
.text:0000000000000F24                               ; __unwind {
.text:0000000000000F24 55                            push    rbp
.text:0000000000000F25 48 89 E5                      mov     rbp, rsp
.text:0000000000000F28 48 83 EC 10                   sub     rsp, 10h
.text:0000000000000F2C 48 89 7D F8                   mov     [rbp+var_8], rdi
.text:0000000000000F30 48 8B 45 F8                   mov     rax, [rbp+var_8]
.text:0000000000000F34 C6 00 01                      mov     byte ptr [rax], 1
.text:0000000000000F37 BF FF FF FF FF                mov     edi, 0FFFFFFFFh                 ; seconds
.text:0000000000000F3C E8 0F FB FF FF                call    _sleep
.text:0000000000000F3C
.text:0000000000000F41 90                            nop
.text:0000000000000F42 C9                            leave
.text:0000000000000F43 C3                            retn
.text:0000000000000F43                               ; } // starts at F24
.text:0000000000000F43
.text:0000000000000F43                               challenge7 endp

这里有两种想法,第一种便是修改rdi,在call sleep前将rdi改为0,第二种便是hook掉sleep,直接return。

def write_rdi_0(ql):
    ql.arch.regs.write("rdi",0)

def my_sleep(ql,*args):
    return

def challenge7(ql):
    base_addr = ql.mem.get_lib_base(ql.path)
    if base_addr is None:
        raise ValueError("base_addr is not set correctly")
    #ql.os.set_api('sleep', my_sleep)
    ql.hook_address(write_rdi_0,base_addr+0x00F3C)

challenge8

_DWORD *__fastcall challenge8(__int64 a1)
{
  _DWORD *result; // rax
  _DWORD *v2; // [rsp+18h] [rbp-8h]

  v2 = malloc(0x18uLL);
  *(_QWORD *)v2 = malloc(0x1EuLL);
  v2[2] = 0x539;
  v2[3] = 0x3DFCD6EA;
  strcpy(*(char **)v2, "Random data");
  result = v2;
  *((_QWORD *)v2 + 2) = a1;
  return result;
}

这一题的target是**Unpack the struct and write at the target address.**解包结构体,然后写入目标地址。

那么我的想法就是在malloc之后hook将rax返回值存入,但是我突然想到,之前我们不是刚学了如何搜索内存的吗,那么我们先通过搜索内存获得地址,然后再写入a1会不会更高端一点。

def search_mem(ql):
    MAGIC=0x3DFCD6EA00000539
    struct_address = ql.mem.search(p64(MAGIC))
    if not struct_address or len(struct_address) < 1:
        raise ValueError("struct_address is not properly initialized")
    mem_value1, mem_value2 ,mem_value3 = struct.unpack("QQQ", ql.mem.read(struct_address[0]-8,0x18))
    print("[*] debug1",hex(mem_value1))
    print("[*] debug2",hex(mem_value2))
    print("[*] debug3",hex(mem_value3))
    ql.mem.write(mem_value3, b"x01")

def challenge8(ql):
    base = ql.mem.get_lib_base(ql.path)
    ql.hook_address(search_mem, base+0xFB5)

challenge9

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");
  src[27] = 0;
  strcpy(dest, src);
  for ( i = dest; *i; ++i )
    *i = tolower(*i);
  *a1 = strcmp(src, dest) == 0;
  return __readfsqword(0x28u) ^ v5;
}

第一个想法就是直接hook掉strcmp函数,或者也可以hook掉tolower函数。

这里尽量hook tolower函数,不然下题就做不了啦。

def my_strcmp(ql,*args) -> int:
    ql.arch.regs.write("rax", 0)

def my_lower(ql,*args):
    ql.arch.regs.rax = ql.arch.regs.rdi

def challenge9(ql):
    #ql.os.set_api('strcmp', my_strcmp,QL_INTERCEPT.EXIT)
    ql.os.set_api('tolower', my_lower,QL_INTERCEPT.EXIT)

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] = ' ';
      }
      buf[v4] = 0;
      if ( !strcmp(buf, "qilinglab") )
        *a1 = 1;
    }
  }
  return __readfsqword(0x28u) ^ v6;
}

想法就是hook掉strcmp,但是上个challenge已经成功hook掉strcmp了,因此就劫持cmdline成一个结构体,直接返回qilinglab即可。

class FakeCmdline(QlFsMappedObject):

    def read(self, size=int) -> bytes:
        return b"qilinglab"

    def close(self) -> int:
        return 0

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

challenge11

.text:0000000000001195 89 75 D0                      mov     [rbp+var_30], esi
.text:0000000000001198 89 4D CC                      mov     [rbp+var_34], ecx
.text:000000000000119B 89 45 D4                      mov     [rbp+var_2C], eax
.text:000000000000119E 81 7D D0 51 69 6C 69          cmp     [rbp+var_30], 696C6951h
.text:00000000000011A5 75 19                         jnz     short loc_11C0
.text:00000000000011A5
.text:00000000000011A7 81 7D CC 6E 67 4C 61          cmp     [rbp+var_34], 614C676Eh
.text:00000000000011AE 75 10                         jnz     short loc_11C0
.text:00000000000011AE
.text:00000000000011B0 81 7D D4 62 20 20 20          cmp     [rbp+var_2C], 20202062h
.text:00000000000011B7 75 07                         jnz     short loc_11C0
.text:00000000000011B7
.text:00000000000011B9 48 8B 45 B8                   mov     rax, [rbp+var_48]
.text:00000000000011BD C6 00 01                      mov     byte ptr [rax], 1

就是让esi==696C6951h && ecx==614C676Eh && eax=20202062h,就可以解决了,所以直接hook掉1195,使得这些寄存器为相应的值。

def set_regs(ql):
    ql.arch.regs.write("esi",0x696C6951)
    ql.arch.regs.write("ecx",0x614C676E)
    ql.arch.regs.write("eax",0x20202062)

def challenge11(ql):
    base = ql.mem.get_lib_base(ql.path)
    ql.hook_address(set_regs, base+0x1195)

如此便解决了所有的挑战。

exp

from qiling import Qiling
from qiling.const import QL_VERBOSE
from qiling.const import QL_INTERCEPT
from qiling.os.mapper import QlFsMappedObject
from pwn import *
import struct

def challenge1(ql):
    ql.mem.map(0x1337//4096*4096 , 0x1000)
    ql.mem.write(0x1337, b"x39x05")

def my_uname_ret(ql ,*args): 
    '''
    struct utsname
    {
        char sysname[65];
        char nodename[65];
        char release[65];
        char version[65];
        char machine[65];
        char domainname[65];
    };
    '''
    rdi_value=ql.arch.regs.read("rdi")
    ql.mem.write(rdi_value,b'QilingOSx00')
    ql.mem.write(rdi_value+65*3,b'ChallengeStartx00')
    return 0

def challenge2(ql):
    ql.os.set_syscall("uname",my_uname_ret,QL_INTERCEPT.EXIT)

class FakeUrandom(QlFsMappedObject):

    def read(self, size=int) -> bytes:
        if size==0x20:
            return b"x02"*32
        # return a constant value upon reading
        return b"x01"

    def fstat(self) -> int:
        # return -1 to let syscall fstat ignore it
        return -1

    def close(self) -> int:
        return 0

def my_getrandom_func(ql, buf, count:int, flag:int) ->int:
    ql.mem.write(buf,b'x02'*count)
    return count

def challenge3(ql):
    ql.add_fs_mapper(r'/dev/urandom', FakeUrandom())
    ql.os.set_syscall("getrandom",my_getrandom_func,QL_INTERCEPT.CALL)

def write_rax_1(ql):
    ql.arch.regs.write("rax",1)

def challenge4(ql):
    base_addr = ql.mem.get_lib_base(ql.path)
    if base_addr is None:
        raise ValueError("base_addr is not set correctly")
    ql.hook_address(write_rax_1,base_addr+0x0E43)

def rand_rets(ql ,*args):
    ql.arch.regs.write("rax",0)

def win(ql: Qiling):
    print('[*] win')

def challenge5(ql):
    ql.os.set_api('rand', rand_rets)
    return

def write_rax_0(ql):
    ql.arch.regs.write("rax",0)

def challenge6(ql):
    base_addr = ql.mem.get_lib_base(ql.path)
    if base_addr is None:
        raise ValueError("base_addr is not set correctly")
    ql.hook_address(write_rax_0,base_addr+0xF16)

def write_rdi_0(ql):
    ql.arch.regs.write("rdi",0)

def my_sleep(ql,*args):
    return

def challenge7(ql):
    base_addr = ql.mem.get_lib_base(ql.path)
    if base_addr is None:
        raise ValueError("base_addr is not set correctly")
    #ql.os.set_api('sleep', my_sleep)
    ql.hook_address(write_rdi_0,base_addr+0x00F3C)

def search_mem(ql):
    MAGIC=0x3DFCD6EA00000539
    struct_address = ql.mem.search(p64(MAGIC))
    if not struct_address or len(struct_address) < 1:
        raise ValueError("struct_address is not properly initialized")
    mem_value1, mem_value2 ,mem_value3 = struct.unpack("QQQ", ql.mem.read(struct_address[0]-8,0x18))
    print("[*] debug1",hex(mem_value1))
    print("[*] debug2",hex(mem_value2))
    print("[*] debug3",hex(mem_value3))
    ql.mem.write(mem_value3, b"x01")

def challenge8(ql):
    base = ql.mem.get_lib_base(ql.path)
    ql.hook_address(search_mem, base+0xFB5)

def my_strcmp(ql,*args) -> int:
    ql.arch.regs.write("rax", 0)

def my_lower(ql,*args):
    ql.arch.regs.rax = ql.arch.regs.rdi

def challenge9(ql):
    #ql.os.set_api('strcmp', my_strcmp,QL_INTERCEPT.EXIT)
    ql.os.set_api('tolower', my_lower,QL_INTERCEPT.EXIT)

class FakeCmdline(QlFsMappedObject):

    def read(self, size=int) -> bytes:
        return b"qilinglab"

    def close(self) -> int:
        return 0

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

def set_regs(ql):
    ql.arch.regs.write("esi",0x696C6951)
    ql.arch.regs.write("ecx",0x614C676E)
    ql.arch.regs.write("eax",0x20202062)

def challenge11(ql):
    base = ql.mem.get_lib_base(ql.path)
    ql.hook_address(set_regs, base+0x1195)

if __name__=='__main__':
    ql = Qiling([r"qilinglab-x86_64"], r'./qiling/examples/rootfs/x8664_linux', verbose=QL_VERBOSE.OFF)
    #ql.verbose = 0
    #ql.debugger = "gdb:0.0.0.0:9999"
    #base_addr = ql.mem.get_lib_base(ql.path)
    #ql.hook_address(win, base_addr + 0x12C5) 
    challenge1(ql)
    challenge2(ql)
    challenge3(ql)
    challenge4(ql)
    challenge5(ql)
    challenge6(ql)
    challenge7(ql)
    challenge8(ql)
    challenge9(ql)
    challenge10(ql)
    challenge11(ql)
    ql.run()
s1nec-1o@s1nec1o:~/Qiling$ python3 solve.py 
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.

Checking which challenge are solved...
Note: Some challenges will results in segfaults and infinite loops if they aren't solved.

Challenge 1: SOLVED
Challenge 2: SOLVED
Challenge 3: SOLVED
Challenge 4: SOLVED
Challenge 5: SOLVED
Challenge 6: SOLVED
[*] debug1 0x55555575a690
[*] debug2 0x3dfcd6ea00000539
[*] debug3 0x80000000dd54
Challenge 7: SOLVED
Challenge 8: SOLVED
Challenge 9: SOLVED
Challenge 10: SOLVED
Challenge 11: SOLVED
You solved 11/11 of the challenges
初探Qiling framework

看雪ID:s1nec-1o

https://bbs.kanxue.com/user-home-989115.htm

*本文为看雪论坛优秀文章,由 s1nec-1o 原创,转载请注明来自看雪社区
#

原文始发于微信公众号(看雪学苑):初探Qiling framework

 

免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉。
  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2025年1月6日22:01:56
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   初探Qiling frameworkhttps://cn-sec.com/archives/3598327.html
                  免责声明:文章中涉及的程序(方法)可能带有攻击性,仅供安全研究与教学之用,读者将其信息做其他用途,由读者承担全部法律及连带责任,本站不承担任何法律及连带责任;如有问题可邮件联系(建议使用企业邮箱或有效邮箱,避免邮件被拦截,联系方式见首页),望知悉.

发表评论

匿名网友 填写信息