前言 做完这些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
方法将连续的内存区域绑定到指定位置,并设置其访问保护位。可以提供字符串标签以便在映射信息表上轻松识别
1 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);
1 ql.mem.unmap(addr: int , size: int ) -> None :
参数: - addr
- 要取消映射的区域基地址 - size
- 区域大小(以字节为单位)
如果请求的内存范围未完全映射,则引发: QlMemoryMappedError
1 address = ql.mem.search(b"\xFF\xFE\xFD\xFC\xFB\xFA" , begin= 0x1000 , end= 0x2000 )
从部分内存范围中搜索字符串,begin和end参数均是可选的,去除则是整个内存范围
1 ql.mem.read(address, size)
从内存中读取
1 ql.mem.write(address, data)
写入内存
1 2 3 def challenge1 (ql) : ql.mem.map(0x1337 //4096 *4096 , 0x1000 ) ql.mem.write(0x1337 , b"\x39\x05" )
challenge2 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 unsigned __int64 __fastcall challenge2 (_BYTE *a1) { unsigned int v2; int v3; int v4; int v5; struct utsname name; char s[10 ]; char v8[24 ]; unsigned __int64 v9; v9 = __readfsqword(0x28 u); 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(0x28 u) ^ v9; }
要求在uname返回系统信息时的sysname == "QilingOS" and version == "ChallengeStart"
。
需要通过劫持结构体
Hijack POSIX 系统调用可以被挂钩以允许用户修改其参数 、改变返回值 或完全替换其功能 。系统调用可以通过其名称或编号进行挂钩,并在一个或多个阶段进行拦截: - QL_INTERCEPT.CALL - :当指定的系统调用即将被调用时,可用于完全替换系统调用功能;**- QL_INTERCEPT.ENTER -** :在进入系统调用之前;可用于篡改系统调用参数值 - QL_INTERCEPT.EXIT - :退出系统调用后,可能被用来篡改返回值
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 from qiling import Qilingfrom qiling.const import QL_INTERCEPTdef my_syscall_write (ql: Qiling, fd: int , buf: int , count: int ) -> int : try : data = ql.mem.read(buf, count) fobj = ql.os.fd[fd] 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 ret if __name__ == "__main__" : ql = Qiling([r'rootfs/arm_linux/bin/arm_hello' ], r'rootfs/arm_linux' ) ql.os.set_syscall('write' , my_syscall_write, QL_INTERCEPT.CALL) ql.run()
因此要通过ql.os.set_syscall中的QL_INTERCEPT.EXIT对返回值进行篡改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 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'QilingOS\x00') ql.mem.write(rdi_value+65*3,b'ChallengeStart\x00') return 0 def challenge2(ql): ql.os.set_syscall("uname",my_uname_ret,QL_INTERCEPT.EXIT)
这里有个要注意的点,my_uname_ret的参数要能接受三个参数,不然会报错
这里看到qiling的一个example
1 onexit_hook(self.ql, *self.get_syscall_args())
Qiling都是通过一些函数,来进行hook的
Qiling的这些hook,都是一些在程序的hook,并不像正常的程序的输入输出,例如这里在退出的地方执行这个oxexit_hook函数(个人理解)
challenge3 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 unsigned __int64 __fastcall challenge3 (_BYTE *a1) { int v2; int i; int fd; char v5; char buf[32 ]; char v7[40 ]; unsigned __int64 v8; v8 = __readfsqword(0x28 u); fd = open ("/dev/urandom" , 0 ); read (fd, buf, 0x20 uLL); 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(0x28 u) ^ v8; }
这里的第一个想法就是hook掉getrandom,控制其函数体,然后将虚拟路径/dev/urandom
映射到文件对象下
以下示例将虚拟路径/dev/urandom
映射到托管系统上现有的/dev/urandom
文件。当模拟程序访问/dev/random
时,将访问映射文件。
1 2 3 4 5 6 7 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
。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 from qiling import Qilingfrom qiling.os.mapper import QlFsMappedObjectclass FakeUrandom (QlFsMappedObject ): def read (self, size: int ) -> bytes : return b"\x04" def fstat (self ) -> int : 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()
注意到
1 2 3 4 5 6 7 8 9 10 11 12 class FakeUrandom (QlFsMappedObject ): def read (self, size: int ) -> bytes : return b"\x04" def fstat (self ) -> int : return -1 def close (self ) -> int : return 0
这里其实是一个类,然后定义了一些函数,用self来代替/dev/urandom
这个对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 class FakeUrandom (QlFsMappedObject ): def read (self, size=int ) -> bytes : if size==0x20 : return b"\x02" *32 return b"\x01" def fstat (self ) -> int : 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 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 .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块上
读寄存器
1 ql.arch.regs.read("EAX" )
从 Unicorn Engine const 读取
1 ql.arch.regs.read(UC_X86_REG_EAX)
1 ql.arch.regs.write("EAX", 0xFF)
通过 Unicorn Engine const 将 0xFF 写入 eax
1 ql.arch.regs.write(UC_X86_REG_EAX, 0xFF )
还有一些跨架构寄存器的获取方法
1 2 3 4 ql.arch.regs.arch_pc ql.arch.regs.arch_sp ql.arch.regs.arch_pc = 0xFF ql.arch.regs.arch_sp = 0xFF
1 ql.arch.regs.register_mapping()
Hook
挂钩具体地址。执行指定地址时将调用已注册的回调。
1 ql.hook_address(回调:可调用,地址:int)
1 2 3 4 5 6 7 8 9 10 11 12 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' ) ql.hook_address(stop, 0x40819a ) ql.run()
挂钩所有说明。注册的回调将在每个汇编指令执行之前调用
1 ql.hook_code(回调:可调用, user_data :任何=无)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 from capstone import Csfrom qiling import Qilingfrom qiling.const import QL_VERBOSEdef 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) ql.hook_code(simple_diassembler, user_data=ql.arch.disassembler) ql.run()
还有一些hook,例如挂钩一段代码、挂钩中断号以调用自定义函数、拦截特定类型的指令等等
我的想法是在
1 .text:0000000000000E43 39 45 FC cmp [rbp+var_4], eax
这个地址执行前,在eax里写入1,这样就使得jl条件达成,就成功跳转到成功片段
1 2 3 4 5 6 7 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 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 unsigned __int64 __fastcall challenge5 (_BYTE *a1) { unsigned int v1; int i; int j; int v5[14 ]; unsigned __int64 v6; v6 = __readfsqword(0x28 u); 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(0x28 u) ^ v6; } } *a1 = 1 ; return __readfsqword(0x28 u) ^ v6; }
这里就是让rand的返回值为0即可
这里不是系统调用的劫持,而是劫持libc函数,这里看个例子
与系统调用一样,POSIX libc 函数可以以类似的方式挂钩,从而允许用户控制其功能。
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 from qiling import Qilingfrom qiling.const import QL_INTERCEPTfrom qiling.os.const import STRINGdef my_puts (ql: Qiling ): params = ql.os.resolve_fcall_params({'s' : STRING}) s = params['s' ] ql.log.info(f'my_puts: got "{s} " as an argument' ) 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()
1 2 3 4 5 6 def rand_rets (ql ,*args ): ql.arch.regs.write("rax" ,0 ) def challenge5 (ql ): ql.os.set_api('rand' , rand_rets) return
challenge6 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 .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即可
1 2 3 4 5 6 7 8 9 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 )
1 2 3 4 5 6 7 8 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 .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
1 2 3 4 5 6 7 8 9 10 11 12 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.hook_address(write_rdi_0,base_addr+0x00F3C )
challenge8 1 2 3 4 5 6 7 8 9 10 11 12 13 14 _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会不会更高端一点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 unsigned __int64 __fastcall challenge9 (bool *a1) { char *i; char dest[32 ]; char src[40 ]; unsigned __int64 v5; v5 = __readfsqword(0x28 u); strcpy (src, "aBcdeFghiJKlMnopqRstuVWxYz" ); src[27 ] = 0 ; strcpy (dest, src); for ( i = dest; *i; ++i ) *i = tolower (*i); *a1 = strcmp (src, dest) == 0 ; return __readfsqword(0x28 u) ^ v5; }
第一个想法就是直接hook掉strcmp函数,或者也可以hook掉tolower函数
这里尽量hook tolower函数,不然下题就做不了啦~~~
1 2 3 4 5 6 7 8 9 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('tolower' , my_lower,QL_INTERCEPT.EXIT)
challenge10 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 unsigned __int64 __fastcall challenge10 (_BYTE *a1) { int i; int fd; ssize_t v4; char buf[72 ]; unsigned __int64 v6; v6 = __readfsqword(0x28 u); fd = open ("/proc/self/cmdline" , 0 ); if ( fd != -1 ) { v4 = read (fd, buf, 0x3F uLL); 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(0x28 u) ^ v6; }
想法就是hook掉strcmp,但是上个challenge已经成功hook掉strcmp了,因此就劫持cmdline成一个结构体,直接返回qilinglab即可
1 2 3 4 5 6 7 8 9 10 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 1 2 3 4 5 6 7 8 9 10 11 12 13 14 .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,使得这些寄存器为相应的值
1 2 3 4 5 6 7 8 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 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 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 from qiling import Qilingfrom qiling.const import QL_VERBOSEfrom qiling.const import QL_INTERCEPTfrom qiling.os.mapper import QlFsMappedObjectfrom pwn import *import structdef challenge1 (ql ): ql.mem.map (0x1337 //4096 *4096 , 0x1000 ) ql.mem.write(0x1337 , b"\x39\x05" ) 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'QilingOS\x00' ) ql.mem.write(rdi_value+65 *3 ,b'ChallengeStart\x00' ) 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 b"\x01" def fstat (self ) -> int : 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.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('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) challenge1(ql) challenge2(ql) challenge3(ql) challenge4(ql) challenge5(ql) challenge6(ql) challenge7(ql) challenge8(ql) challenge9(ql) challenge10(ql) challenge11(ql) ql.run()
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 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