QilingLab Quickstart

前言

此Lab用于快速熟悉Qiling框架的主要功能,其拥有两个版本x86-64以及aarch64两个版本,获取可以访问下面的页面

Shielder - QilingLab – Release

其包含11个挑战:

1
2
3
4
5
6
7
8
9
10
11
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.

使用IDA打开看到的也是没有去除对应的符号信息,也没有混淆,可以比较直观的看到对应的检测:

基本用法

根据互联网上的资料以及项目中对应的说明文档,我们需要使用到rootfs中的东西,其为Qiling框架测试所需要的文件

我们使用以下命令进行clone Qiling项目的框架(使用--recursiv来循环拉取子项目):

1
git clone https://github.com/qilingframework/qiling.git --recursiv

之后我们在后续中使用到的脚本模板如下:

1
2
3
4
5
6
7
8
9
10
11
from qiling import *

def challenge1(ql: Qiling):
pass

if __name__ == '__main__':
path = ['qilinglab-x86_64'] # 我们的目标
rootfs = "./qiling/examples/rootfs/x8664_linux" # 在你clone下来的仓库里
ql = Qiling(path, rootfs)
challenge1(ql) # 在ql.run()之前,做好我们的hook工作
ql.run()

Challenge1:修改内存

1
2
3
4
5
void __fastcall challenge1(_BYTE *a1)
{
if ( MEMORY[0x1337] == 1337 )
*a1 = 1;
}

可以看出来需要我们在地址为0x1337处存放一个值为1337,对此我们需要使用以下代码来进行映射一块内存:

1
2
3
4
5
ql.mem.map(0x1000, 0x1000, info='[challenge1]')
# ql.mem.map(addr: int, size: int, perms: int = UC_PROT_ALL, info: Optional[str] = None) -> None
#addr:映射基地址
#size:以字节为单位的映射大小
#info:为映射范围设置字符串标签,以便于识别

因为 Qiling 框架底层上还是使用的 Unicorn 那一套,所以还是需要按 4k 进行对齐

内存操作相关参考:Memory - Qiling Framework Documentation

我们需要写入内存的时候使用以下代码:

1
ql.mem.write(address, data)

需要注意的是,当我们写入数据的时候需要将对应的数据进行打包,在这个挑战中我们需要将其打包为16位数据无符号,即两字节:

1
ql.pack16()  # unsigned short 类型数据

当我们需要使用有符号的时候在对应数字后面加一个s即可,如ql.pack16s则是有符号的short类型数据

打包与解包参考:Pack and Unpack - Qiling Framework Documentation

对应解题脚本如下:

1
2
3
def Challenge1(ql: Qiling):
ql.mem.map(0x1000,0x1000,info="[Challenge 1]")
ql.mem.write(0x1337, ql.pack16(1337))

可以看到已经成功解决(此时运行于 Windows 环境下)

可以在main函数的运行代码处加上下面的代码来开启于关闭对应调试模式

1
ql.verbose = 0 # 0: no debug info, 1: debug info

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来进行获取系统信息,通过对比系统信息来进行校验。我们可以在uname所获取的信息为一个utsname的结构体,我们可以在IDA中进行看到对应的结构信息

对应的我们可以在系统调用返回的时候进行Hook,打到我们想要的功能

劫持系统调用参考文档:Hijack - Qiling Framework Documentation

HOOK操作参考文档:Hook - Qiling Framework Documentation

寄存器相关的操作参考文档:Register - Qiling Framework Documentation

在上述的代码中我们进行简单的调试可以看到对应uname获取信息后存储在rdi寄存器中,那么我们的目标便是修改uname执行完毕后的rdi所指向的地址空间

那么对应的Hook代码如下(运行于Linux下):

1
2
3
4
5
6
7
8
9
10
11
12
13
from qiling import *
from qiling.const import *
from qiling.os.const import *

def hook_rdi(ql: Qiling, *args):
rdi = ql.arch.regs.rdi # 获取寄存器的值
ql.mem.write(rdi, b'QilingOS\x00') # 写入数据
ql.mem.write(rdi + 65 * 3, b'ChallengeStart\x00') # 根据uname的结构体,写入数据

def Challenge2(ql: Qiling):
# hook系统调用返回
# QL_INTERCEPT.EXIT 在退出系统调用后
ql.os.set_syscall('uname', hook_rdi, QL_INTERCEPT.EXIT)

之后我们可以尝试运行对应脚本,观察输出(后续运行均于 Window WSL 下)

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

可以看到对应程序从/dev/urandom获取了随机数,之后再通过getrandom()来获取随机数,之后要求满足上述对应的关系,同时有一个随机数和其他的都不一样

Qiling提供了QlFsMappedObject去自定义文件系统,例:read,write等

同样可以参考系统劫持的文档:Hijack - Qiling Framework Documentation

那么对于这道题的核心便是我们自己手动实现一个read,并且Hook函数getrandom

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from qiling import *
from qiling.const import *
from qiling.os.const import *
from qiling.os.mapper import QlFsMappedObject

class my_uradom(QlFsMappedObject):
def read(self, size):
if size == 1:
return b'\x50' # 设置为 v5 的随机值
else:
return b'\x00' * size # 设置为 buf 的随机值

def close(self):
return 0

def hook_getrandom(ql:Qiling, buf, size, flags):
ql.mem.write(buf, b'\x00' * size) # 设置为 v7 的随机值
ql.os.set_syscall_return(0)

def Challenge3(ql:Qiling):
ql.os.set_syscall('getrandom', hook_getrandom, QL_INTERCEPT.CALL) # hook getrandom
ql.add_fs_mapper('/dev/urandom', my_uradom())

Challenge4:Hook地址

1
2
3
4
__int64 challenge4()
{
return 0LL;
}

在IDA的反编译中我们无法看到对应的伪代码,但是当我们切换到汇编界面的时候可以看到对应的代码:

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:0000560C8A000E1D public challenge4
.text:0000560C8A000E1D challenge4 proc near ; CODE XREF: start+18F↓p
.text:0000560C8A000E1D
.text:0000560C8A000E1D var_18= qword ptr -18h
.text:0000560C8A000E1D var_8= dword ptr -8
.text:0000560C8A000E1D var_4= dword ptr -4
.text:0000560C8A000E1D
.text:0000560C8A000E1D ; __unwind { // 560C8A000000
.text:0000560C8A000E1D push rbp
.text:0000560C8A000E1E mov rbp, rsp
.text:0000560C8A000E21 mov [rbp+var_18], rdi
.text:0000560C8A000E25 mov [rbp+var_8], 0
.text:0000560C8A000E2C mov [rbp+var_4], 0
.text:0000560C8A000E33 jmp short loc_560C8A000E40
.text:0000560C8A000E33
.text:0000560C8A000E35 ; ---------------------------------------------------------------------------
.text:0000560C8A000E35
.text:0000560C8A000E35 loc_560C8A000E35: ; CODE XREF: challenge4+29↓j
.text:0000560C8A000E35 mov rax, [rbp+var_18]
.text:0000560C8A000E39 mov byte ptr [rax], 1
.text:0000560C8A000E3C add [rbp+var_4], 1
.text:0000560C8A000E3C
.text:0000560C8A000E40
.text:0000560C8A000E40 loc_560C8A000E40: ; CODE XREF: challenge4+16↑j
.text:0000560C8A000E40 mov eax, [rbp+var_8]
.text:0000560C8A000E43 cmp [rbp+var_4], eax
.text:0000560C8A000E46 jl short loc_560C8A000E35
.text:0000560C8A000E46
.text:0000560C8A000E48 nop
.text:0000560C8A000E49 pop rbp
.text:0000560C8A000E4A retn
.text:0000560C8A000E4A ; } // starts at 560C8A000E1D
.text:0000560C8A000E4A
.text:0000560C8A000E4A challenge4 endp

可以明显的看到程序会给eax赋值为 0,之后在jl处不执行对应的跳转,对应的我们在加载偏移为0xE43处将eax的值进行Hook,修改为 1,之后让其进入循环,将我们的Check赋值为 1

对应的脚本如下:

1
2
3
4
5
6
7
def hook_eax(ql:Qiling):
ql.arch.regs.eax = 1 # 设置 eax 为1

def Challenge4(ql:Qiling):
libc_base = ql.mem.get_lib_base(ql.path) # 获取libc的基址
hook_addr = libc_base + 0xE43
ql.hook_address(hook_eax, hook_addr) # hook_address(回调函数, hook 地址)

在使用ql.path时候有个天坑,当使用此方法时需要注意你初始化Qiling的文件路径,下面为正确示范:

1
2
3
path = ["qilinglab-x86_64"]
rootfs = "./qiling/examples/rootfs/x8664_linux"
ql = Qiling(path, rootfs)

下面为错误示范:

1
2
3
path = ["./qilinglab-x86_64"]
rootfs = "./qiling/examples/rootfs/x8664_linux"
ql = Qiling(path, rootfs)

运行脚本后结果如下:

Challenge5:Hook外部函数

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()进行Hook处理,将对应的返回值修改为 0,对此我们可以使用下面的代码来进行Hook处理

1
ql.set_api(Hook函数名, 回调函数, QL_INTERCEPT.CALL)

对此我们的脚本如下:

1
2
3
4
5
def hook_rand(ql: Qiling):
ql.reg.rax = 0

def challenge5(ql: Qiling):
ql.set_api('rand', hook_rand)

运行后因为Challenge6为死循环,会卡住对应的输出。所以没有相关的回显

Challenge6:突破死循环

1
2
3
4
5
void challenge6()
{
while ( 1 )
;
}

我们切换至汇编可以看到其逻辑:

我们需要做的便是在其循环处修改al的值,让其跳出循环,解题脚本与Challenge4相似:

1
2
3
4
5
6
def hook_while_true(ql: Qiling):
ql.reg.rax = 0

def challenge6(ql: Qiling):
libc_base = ql.mem.get_lib_base(ql.path)
ql.hook_address(hook_while_true, libc_base + 0xF16)

此题也没有回显,因为在Challenge7中存在有一个Sleep函数,会拖很久的时间

Challenge7:解决睡觉难题

1
2
3
4
5
unsigned int __fastcall challenge7(_BYTE *a1)
{
*a1 = 1;
return sleep(0xFFFFFFFF);
}

可以看到其有一个超长的 Sleep 函数,对于这个我们有几种修改方式:

  1. 修改sleep的参数值,减少睡眠时间

  2. 通过 Hook 自己实现 API sleep

  3. 修改系统调用nanosleep()直接返回,因为sleep()底层调用的nanosleep()

    # man 3 sleep

    NOTES On Linux, sleep() is implemented via nanosleep(2). See the nanosleep(2)

    man page for a discussion of the clock used.

对于方法一,我们可以看到是传入了edi作为对应sleep的参数,那么我们修改参数其实就是要修改edi的值,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
def hook_edi(ql:Qiling):
ql.arch.regs.rdi = 0

def Challenge7(ql: Qiling):
# Hook 地址
libc_base = ql.mem.get_lib_base(ql.path)
hook_addr = libc_base + 0xF3C
ql.hook_address(hook_edi, hook_addr)

# Hook 函数执行前
ql.set_api('sleep', hook_edi, QL_INTERCEPT.ENTER)

对于方法二,我们将对应的sleep函数进行掉包成我们自己的:

1
2
3
4
5
def hook_sleep(ql: Qiling):
return #直接返回

def Challenge7(ql: Qiling):
ql.set_api('sleep', hook_sleep)

对于方法三 Hook 系统调用即可:

1
2
3
4
5
def hook_nanosleep(ql: Qiling, *args, **kwargs):
return #直接返回

def Challenge7(ql: Qiling):
ql.os.set_syscall('nanosleep', hook_nanosleep)

Challenge 8: 解析结构体,往正确地址写入值

1
2
3
4
5
6
7
8
9
10
11
void __fastcall challenge8(__int64 a1)
{
_DWORD *v1; // [rsp+18h] [rbp-8h]

v1 = malloc(0x18uLL);
*v1 = malloc(0x1EuLL);
v1[2] = 1337;
v1[3] = 1039980266;
strcpy(*v1, "Random data");
*(v1 + 2) = a1;
}

大概可以看出来是一个结构体数据,我们在IDA中创建一个结构体,将v1的类型设置为我们所创建的结构体数据

之后修改v1的类型,可以看到整个代码好看多了:

1
2
3
4
5
6
7
8
9
10
void __fastcall challenge8(__int64 a1)
{
struc_1 *v1; // [rsp+18h] [rbp-8h]

v1 = malloc(24uLL);
v1->some_string = malloc(0x1EuLL);
v1->magic = 0x3DFCD6EA00000539LL;
strcpy(v1->some_string, "Random data");
v1->check_addr = a1;
}

对应需要用到结构体的解包相关知识以及字节顺序、大小、对齐方式三种

结构体解包文档参考:Pack and Unpack - Qiling Framework Documentation

字节顺序、大小、对齐方式参考文档:

struct — Interpret bytes as packed binary data — Python 3.12.1 documentation

struct — Interpret bytes as packed binary data — Python 3.12.1 documentation

struct — Interpret bytes as packed binary data — Python 3.12.1 documentation

我们简单的调试一下可以看到对应的rax可以看到其保存了结构体的首地址信息,那么我们需要将rax+0x10处的地址里的数据进行写入为 1

同时我们可以开启调试模式来进行观察所找到的数据是否正确(即开启ql.verbose = 1),对应脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def hook_mem(ql:Qiling):
rax = ql.arch.regs.rax # 结构体首地址
ql.log.info(f"\u001b[31m[+] rax: {hex(rax)}\u001b[0m")

data = ql.mem.read(rax, 24) # 读取结构体数据
ql.log.info(f"\u001b[31m[+] data: {data.hex()}\u001b[0m")

str_addr, magic_num, check_addr = struct.unpack("QQQ", data) # 解析结构体数据
ql.log.info(f"\u001b[31m[+] str_addr: {hex(str_addr)}\u001b[0m")
ql.mem.read(str_addr, 0x10) # 读取字符串数据
ql.log.info(f"\u001b[31m[+] str: {ql.mem.string(str_addr)}\u001b[0m")
ql.log.info(f"\u001b[31m[+] magic_num: {hex(magic_num)}\u001b[0m")
ql.log.info(f"\u001b[31m[+] check_addr: {hex(check_addr)}\u001b[0m")

ql.mem.write(check_addr, b'\x01') # 将check_addr的值设置为1
check = ql.mem.read(check_addr, 8) # 读取check_addr的值
ql.log.info(f"\u001b[31m[+] check: {check.hex()}\u001b[0m")


def Challenge8(ql: Qiling):
libc_base = ql.mem.get_lib_base(ql.path)
hook_addr = libc_base + 0x00FB5
ql.log.info(f"\u001b[31m[+] hook_addr: {hook_addr}\u001b[0m")
ql.hook_address(hook_mem, hook_addr)

输出时以\u001b[31m开头,\u001b[0m结尾会将中间内容转换为红色,更多颜色可以参考 ANSI 码,此处不多描述

之后我们可以看到以下界面:

总感觉 Qiling 自带的 Debug 有点没用?

总的来说开启了 Debug 后还是得自己插桩写入 log,同时 log 也没点颜色…得自己设置一个颜色才好看的多,还方便找

对于上面的内容其实还一种解题方式,可以在内存中搜索对应数据信息,因为题目给的magic数字以及对应标志字符串Random data可以很快的帮助我们在内存里面找到对应的信息,脚本如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def search_mem_to_find_struct(ql: Qiling):
MAGIC = ql.pack64(0x3DFCD6EA00000539)
candidate_addrs = ql.mem.search(MAGIC)

for addr in candidate_addrs:
# 有可能有多个地址,所以通过其他特征进一步确认
stru_addr = addr - 8
stru = ql.mem.read(stru_addr, 24)
string_addr, _, check_addr = struct.unpack('QQQ', stru)
if ql.mem.string(string_addr) == 'Random data':
ql.mem.write(check_addr, b'\x01')
break

def Challenge8(ql: Qiling):
base = ql.mem.get_lib_base(ql.path)
ql.hook_address(search_mem_to_find_struct, base + 0xFB5)

Challenge 9: 修改字符串函数

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
void __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 v4; // [rsp+68h] [rbp-8h]

v4 = __readfsqword(0x28u);
strcpy(src, "aBcdeFghiJKlMnopqRstuVWxYz");
src[27] = unk_562F9A0016C0;
strcpy(dest, src);
for ( i = dest; *i; ++i )
*i = tolower(*i);
*a1 = strcmp(src, dest) == 0;
}

可以直观的看到对应的我们需要将一个转小写的函数tolower()进行 Hook 将其内容不做处理通过下面的strcmp()函数,当然我们也可以 Hook strcmp()这个函数,让其直接返回 0

1
2
3
4
5
def hook_tolower(ql: Qiling, *args):
return # 不执行tolower函数

def Challenge9(ql: Qiling):
ql.os.set_api('tolower', hook_tolower, QL_INTERCEPT.CALL)

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

这个感觉上又回到了Challenge3,同样是要求我们 Hook 文件系统,那么还是一样的我们可以自己实现一个cmdline的读取与关闭

1
2
3
4
5
6
7
8
9
10
11
12
class My_cmdline(QlFsMappedObject):
def read(self, size):
if size == 63:
return b'qilinglab'
else:
return b'\x00' * size

def close(self):
return 0

def Challenge10(ql: Qiling):
ql.add_fs_mapper('/proc/self/cmdline', My_cmdline)

还有其他的省事的办法,比如可以将我们自己的文件通过ql.add_fs_mapper(源文本, Hook的自定义文本)来进行替换

Challenge 11: 指令Hook

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void __fastcall challenge11(_BYTE *a1)
{
int v6; // [rsp+1Ch] [rbp-34h]
int v7; // [rsp+24h] [rbp-2Ch]
char s[4]; // [rsp+2Bh] [rbp-25h] BYREF
char v9[4]; // [rsp+2Fh] [rbp-21h] BYREF
char v10[4]; // [rsp+33h] [rbp-1Dh] BYREF
unsigned __int64 v11; // [rsp+38h] [rbp-18h]

v11 = __readfsqword(0x28u);
_RAX = 0x40000000LL;
__asm { cpuid } // 获取CPU信息
v6 = _RCX;
v7 = _RDX;
if ( __PAIR64__(_RBX, _RCX) == 0x696C6951614C676ELL && _RDX == 538976354 )
*a1 = 1;
sprintf(s, "%c%c%c%c", _RBX, (_RBX >> 8), (_RBX >> 16), (_RBX >> 24));
sprintf(v9, "%c%c%c%c", v6, (v6 >> 8), (v6 >> 16), (v6 >> 24));
sprintf(v10, "%c%c%c%c", v7, (v7 >> 8), (v7 >> 16), (v7 >> 24));
}

通过观察汇编我们可以看到对应的相当是实现对寄存器的修改,那么相当于是我们需要对cpuid这个指令进行 Hook 处理

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
.text:0000562F9A001175 C7 45 CC 00 00 00 00          mov     [rbp+var_34], 0
.text:0000562F9A00117C C7 45 D0 00 00 00 00 mov [rbp+var_30], 0
.text:0000562F9A001183 C7 45 D4 00 00 00 00 mov [rbp+var_2C], 0
.text:0000562F9A00118A B8 00 00 00 40 mov eax, 40000000h
.text:0000562F9A00118F 0F A2 cpuid
.text:0000562F9A001191 89 D0 mov eax, edx
.text:0000562F9A001193 89 DE mov esi, ebx
.text:0000562F9A001195 89 75 D0 mov [rbp+var_30], esi
.text:0000562F9A001198 89 4D CC mov [rbp+var_34], ecx
.text:0000562F9A00119B 89 45 D4 mov [rbp+var_2C], eax
.text:0000562F9A00119E 81 7D D0 51 69 6C 69 cmp [rbp+var_30], 696C6951h
.text:0000562F9A0011A5 75 19 jnz short loc_562F9A0011C0
.text:0000562F9A0011A5
.text:0000562F9A0011A7 81 7D CC 6E 67 4C 61 cmp [rbp+var_34], 614C676Eh
.text:0000562F9A0011AE 75 10 jnz short loc_562F9A0011C0
.text:0000562F9A0011AE
.text:0000562F9A0011B0 81 7D D4 62 20 20 20 cmp [rbp+var_2C], 20202062h
.text:0000562F9A0011B7 75 07 jnz short loc_562F9A0011C0
.text:0000562F9A0011B7
.text:0000562F9A0011B9 48 8B 45 B8 mov rax, [rbp+var_48]
.text:0000562F9A0011BD C6 00 01 mov byte ptr [rax], 1
.text:0000562F9A0011BD
.text:0000562F9A0011C0 loc_562F9A0011C0: ; CODE XREF: challenge11+4C↑j
.text:0000562F9A0011C0 8B 45 D0 mov eax, [rbp+var_30]
.text:0000562F9A0011C3 C1 F8 18 sar eax, 18h
.text:0000562F9A0011C6 89 C7 mov edi, eax
.text:0000562F9A0011C8 8B 45 D0 mov eax, [rbp+var_30]
.text:0000562F9A0011CB C1 F8 10 sar eax, 10h
.text:0000562F9A0011CE 89 C6 mov esi, eax
.text:0000562F9A0011D0 8B 45 D0 mov eax, [rbp+var_30]
.text:0000562F9A0011D3 C1 F8 08 sar eax, 8
.text:0000562F9A0011D6 89 C1 mov ecx, eax
.text:0000562F9A0011D8 8B 55 D0 mov edx, [rbp+var_30]
.text:0000562F9A0011DB 48 8D 45 DB lea rax, [rbp+s]
.text:0000562F9A0011DF 41 89 F9 mov r9d, edi
.text:0000562F9A0011E2 41 89 F0 mov r8d, esi
.text:0000562F9A0011E5 48 8D 35 F2 04 00 00 lea rsi, format ; "%c%c%c%c"
.text:0000562F9A0011EC 48 89 C7 mov rdi, rax ; s
.text:0000562F9A0011EF B8 00 00 00 00 mov eax, 0
.text:0000562F9A0011F4 E8 37 F8 FF FF call _sprintf

按照之前修改寄存器的方式进行绕过,则我们可以写出如下脚本:

1
2
3
4
5
6
7
8
9
def hook_cpuid(ql: Qiling, *args):
ql.arch.regs.ebx = 0x696C6951
ql.arch.regs.ecx = 0x614C676E
ql.arch.regs.edx = 0x20202062

def Challenge11(ql: Qiling):
libc_base = ql.mem.get_lib_base(ql.path)
hook_addr = libc_base + 0x1191
ql.hook_address(hook_cpuid, hook_addr)

但是实际上我们还可以对cpuid处指令进行 Hook,对应实现代码如下:

1
2
3
4
5
6
7
8
9
def hook_cpuid(ql: Qiling, address, size):
if ql.mem.read(address, size) == b'\x0F\xA2': # 对应的 cpuid 指令字节码
ql.arch.regs.ebx = 0x696C6951
ql.arch.regs.ecx = 0x614C676E
ql.arch.regs.edx = 0x20202062
ql.arch.regs.rip += 2

def Challenge11(ql: Qiling):
ql.hook_code(hook_cpuid)

运行后我们可以看到以下内容:

至此我们的全部挑战已经完成,对应的完整脚本如下:

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
from qiling import *
from qiling.const import *
from qiling.os.const import *
from qiling.os.mapper import QlFsMappedObject
import struct

def Challenge1(ql: Qiling):
ql.mem.map(0x1000,0x1000,info="[Challenge 1]")
ql.mem.write(0x1337, ql.pack16(1337))

def hook_rdi(ql: Qiling, *args):
rdi = ql.arch.regs.rdi # 获取寄存器的值
ql.mem.write(rdi, b'QilingOS\x00') # 写入数据
ql.mem.write(rdi + 65 * 3, b'ChallengeStart\x00') # 根据uname的结构体,写入数据

def Challenge2(ql: Qiling):
# hook系统调用返回
# QL_INTERCEPT.EXIT 在退出系统调用后
ql.os.set_syscall('uname', hook_rdi, QL_INTERCEPT.EXIT)

class My_uradom(QlFsMappedObject):
def read(self, size):
if size == 1:
return b'\x50' # 设置为 v5 的随机值
else:
return b'\x00' * size # 设置为 buf 的随机值

def close(self):
return 0

def hook_getrandom(ql:Qiling, buf, size, flags):
ql.mem.write(buf, b'\x00' * size) # 设置为 v7 的随机值
ql.os.set_syscall_return(0)

def Challenge3(ql:Qiling):
ql.os.set_syscall('getrandom', hook_getrandom, QL_INTERCEPT.CALL) # hook getrandom
ql.add_fs_mapper('/dev/urandom', My_uradom())

def hook_eax(ql:Qiling):
ql.arch.regs.eax = 1 # 设置 eax 为1

def Challenge4(ql:Qiling):
libc_base = ql.mem.get_lib_base(ql.path) # 获取libc的基址
ql.log.info(f"ql_path: {ql.path}")
ql.log.info(f"libc_base: {libc_base}")
hook_addr = libc_base + 0xE43
ql.hook_address(hook_eax, hook_addr)

def hook_rand(ql:Qiling, *args):
ql.arch.regs.rax = 0 # 设置 rax 为 0

def Challenge5(ql:Qiling):
ql.os.set_api('rand', hook_rand, QL_INTERCEPT.CALL)

def hook_while_true(ql: Qiling):
ql.arch.regs.rax = 0

def challenge6(ql: Qiling):
libc_base = ql.mem.get_lib_base(ql.path)
ql.hook_address(hook_while_true, libc_base + 0xF16)

def hook_sleep(ql: Qiling, *args):
ql.os.set_syscall_return(0)

def hook_edi(ql:Qiling):
ql.arch.regs.rdi = 0

def Challenge7(ql: Qiling):
libc_base = ql.mem.get_lib_base(ql.path)
hook_addr = libc_base + 0xF3C
ql.hook_address(hook_edi, hook_addr)
# ql.os.set_syscall('nanosleep', hook_sleep, QL_INTERCEPT.CALL)

def hook_mem(ql:Qiling):
rax = ql.arch.regs.rax # 结构体首地址
ql.log.info(f"\u001b[31m[+] rax: {hex(rax)}\u001b[0m")

data = ql.mem.read(rax, 24) # 读取结构体数据
ql.log.info(f"\u001b[31m[+] data: {data.hex()}\u001b[0m")

str_addr, magic_num, check_addr = struct.unpack("QQQ", data) # 解析结构体数据
ql.log.info(f"\u001b[31m[+] str_addr: {hex(str_addr)}\u001b[0m")
ql.mem.read(str_addr, 0x10) # 读取字符串数据
ql.log.info(f"\u001b[31m[+] str: {ql.mem.string(str_addr)}\u001b[0m")
ql.log.info(f"\u001b[31m[+] magic_num: {hex(magic_num)}\u001b[0m")
ql.log.info(f"\u001b[31m[+] check_addr: {hex(check_addr)}\u001b[0m")

ql.mem.write(check_addr, b'\x01') # 将check_addr的值设置为1
check = ql.mem.read(check_addr, 8) # 读取check_addr的值
ql.log.info(f"\u001b[31m[+] check: {check.hex()}\u001b[0m")


def Challenge8(ql: Qiling):
libc_base = ql.mem.get_lib_base(ql.path)
hook_addr = libc_base + 0x00FB5
ql.log.info(f"\u001b[31m[+] hook_addr: {hook_addr}\u001b[0m")
ql.hook_address(hook_mem, hook_addr)


def hook_tolower(ql: Qiling, *args):
return # 不执行tolower函数

def Challenge9(ql: Qiling):
ql.os.set_api('tolower', hook_tolower, QL_INTERCEPT.CALL)


class My_cmdline(QlFsMappedObject):
def read(self, size):
if size == 63:
return b'qilinglab'
else:
return b'\x00' * size

def close(self):
return 0

def Challenge10(ql: Qiling):
ql.add_fs_mapper('/proc/self/cmdline', My_cmdline)

def hook_cpuid(ql: Qiling, *args):
ql.arch.regs.ebx = 0x696C6951
ql.arch.regs.ecx = 0x614C676E
ql.arch.regs.edx = 0x20202062

def Challenge11(ql: Qiling):
libc_base = ql.mem.get_lib_base(ql.path)
hook_addr = libc_base + 0x1191
ql.hook_address(hook_cpuid, hook_addr)

if __name__ == "__main__":
# Windows
# path = ["qilinglab-x86_64"]
# rootfs = ".\\qiling\\examples\\rootfs\\x8664_linux"

# Linux
path = ["qilinglab-x86_64"]
rootfs = "./qiling/examples/rootfs/x8664_linux"
ql = Qiling(path, rootfs)

Challenge1(ql)
Challenge2(ql)
Challenge3(ql)
Challenge4(ql)
Challenge5(ql)
challenge6(ql)
Challenge7(ql)
Challenge8(ql)
Challenge9(ql)
Challenge10(ql)
Challenge11(ql)

ql.verbose = 0 # 0: no debug info, 1: debug info
ql.run()

写的比较丑陋,大家看看就行

总结

总的来说Qiling Lab相当于是带着学习入门使用的一个过程,带着我们学习了内存的修改、地址的Hook、文件系统的劫持、系统函数,外部函数的Hook修改。基本上把Qiling框架所常用的都用到了。

在此学习过程中更多的时候还是需要参考官方的手册文档来进行,通过多练来达到熟悉的作用。

菜 就多练
输不起 就别玩
以前是以前
现在是现在
你要是一直拿以前当做现在
哥们儿 你怎么不拿你刚出生的时候对比啊

​ ——广君


QilingLab Quickstart
https://equinox-shame.github.io/2024/01/03/QilingLab Quickstart/
作者
梓曰
发布于
2024年1月3日
许可协议