前言

做完这些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; // [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 - :退出系统调用后,可能被用来篡改返回值

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 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对返回值进行篡改

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; // [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时,将访问映射文件。

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 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()

注意到

1
2
3
4
5
6
7
8
9
10
11
12
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这个对象

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 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

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块上

读寄存器

  • 从字符串“eax”读取
1
ql.arch.regs.read("EAX")
  • 从 Unicorn Engine const 读取
1
ql.arch.regs.read(UC_X86_REG_EAX)
  • 读eax
1
eax = ql.arch.regs.eax
  • 将 0xFF 写入“eax”
1
ql.arch.regs.write("EAX", 0xFF)
  • 通过 Unicorn Engine const 将 0xFF 写入 eax
1
ql.arch.regs.write(UC_X86_REG_EAX, 0xFF)
  • 将 0xFF 写入 eax
1
ql.arch.regs.eax =  0xFF

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

1
2
3
4
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寄存器表列表
1
ql.arch.regs.register_mapping()
  • 在 64 位环境中,这将返回 64
1
ql.arch.reg_bits("rax")
  • 在 64 位环境中,这将返回 32
1
ql.arch.reg_bits("eax")

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')

# have 'stop' called when execution reaches 0x40819a
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 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,例如挂钩一段代码、挂钩中断号以调用自定义函数、拦截特定类型的指令等等

我的想法是在

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; // 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 函数可以以类似的方式挂钩,从而允许用户控制其功能。

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 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()

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.os.set_api('sleep', my_sleep)
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; // [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函数,不然下题就做不了啦~~~

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('strcmp', my_strcmp,QL_INTERCEPT.EXIT)
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; // [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即可

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 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"\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 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()
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