0x00 前言

第一次接触模拟执行这个东西,从QilingFramework开始上手试试看,后面还要学Qemu、Unicorn、Unidbg等

0x01 解

使用

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

克隆整个仓库

由于x86_64aarch64题目内容,而我使用的是x86_64平台,因此为了体现模拟执行的强大,以下都使用aarch64的文件来解

之后使用模板如下

1
2
3
4
5
6
7
8
9
10
11
12
13
# 模板如下:
from qiling import *
from qiling.const import QL_VERBOSE

def challenge1(ql: Qiling):
pass

if __name__ == '__main__':
path = ['qilinglab/qilinglab-aarch64'] # lab目标
rootfs = "./examples/rootfs/arm64_linux" # 在clone下来的仓库里
ql = Qiling(path, rootfs,verbose=QL_VERBOSE.OFF) # 关闭了VERBOSE,否则输出太难看了
challenge1(ql) # 在ql.run()之前,做好hook工作
ql.run()

Challenge 1: Store 1337 at pointer 0x1337.

1
2
3
4
5
6
7
8
9
10
11
12
_BYTE *__fastcall challenge1(_BYTE *a1)
{
_BYTE *result; // x0

result = (_BYTE *)*(unsigned int *)((char *)&loc_1334 + 3);
if ( *(_DWORD *)((char *)&loc_1334 + 3) == 1337 )
{
result = a1;
*a1 = 1;
}
return result;
}

将值1337放入地址0x1337中,然而实际上并不能保证程序加载基地址,这里需要用

1
ql.mem.map(0x1000, 0x1000, info='[challenge]')

映射一块内存,需要注意的是,Qiling底层就是用的Unicorn Engine,内存映射时,要4k对齐

之后向里面写内容就可以了,注意写的时候内容要进行打包

1
2
3
def challenge1(ql: Qiling):
ql.mem.map(0x1000,0x1000)
ql.mem.write(0x1337,ql.pack16(1337))

这就能过第一个了

Challenge 2: Make the ‘uname’ syscall return the correct values.

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
__int64 __fastcall challenge2(_BYTE *a1)
{
unsigned int v3; // [xsp+30h] [xbp+30h]
int v4; // [xsp+34h] [xbp+34h]
int v5; // [xsp+38h] [xbp+38h]
int v6; // [xsp+3Ch] [xbp+3Ch]
struct utsname name; // [xsp+40h] [xbp+40h] BYREF
char s[16]; // [xsp+1C8h] [xbp+1C8h] BYREF
char v9[16]; // [xsp+1D8h] [xbp+1D8h] BYREF
__int64 v10; // [xsp+1E8h] [xbp+1E8h]

if ( uname(&name) )
{
perror("uname");
}
else
{
strcpy(s, "QilingOS");
s[9] = 0;
strcpy(v9, "ChallengeStart");
v9[15] = 0;
v3 = 0;
v4 = 0;
while ( v5 < strlen(s) )
{
if ( name.sysname[v5] == s[v5] )
++v3;
++v5;
}
while ( v6 < strlen(v9) )
{
if ( name.version[v6] == v9[v6] )
++v4;
++v6;
}
if ( v3 == strlen(s) && v4 == strlen(v9) && v3 > 5 )
*a1 = 1;
}
return v10 ^ _stack_chk_guard;
}

这边是需要让uname这个syscall返回的内容与设置的相同,设置的值为sysname==QilingOSversion==ChallengeStart

uname这个函数传入一个utsname的结构体的地址,填充后返回

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <sys/utsname.h>
#define _UTSNAME_SYSNAME_LENGTH 65
struct utsname{
char sysname[_UTSNAME_SYSNAME_LENGTH];//当前操作系统名
char nodename[_UTSNAME_NODENAME_LENGTH];//网络上的名称
char release[_UTSNAME_RELEASE_LENGTH];//当前发布级别
char version[_UTSNAME_VERSION_LENGTH];//当前发布版本
char machine[_UTSNAME_MACHINE_LENGTH];//当前硬件体系类型
#if _UTSNAME_DOMAIN_LENGTH - 0
/* Name of the domain of this node on the network. */
# ifdef __USE_GNU
char domainname[_UTSNAME_DOMAIN_LENGTH]; //当前域名
# else
char __domainname[_UTSNAME_DOMAIN_LENGTH];
# endif
#endif
};

Qiling提供了在系统调用返回时进行hook功能

首先需要知道返回值被存储在了哪个寄存器(因为这是ARM64,所以跟X64不一样)

根据汇编

1
2
3
4
5
...
name= -0x1B0
...
ADD X0, SP, #0x1F0+name ; add r2, r1, #2 (r2 = r1 + 2)
BL .uname

可以知道返回值由SP0x1F0+name的值相加得到,被存在了X0寄存器,所以劫持地址为

1
x0 = ql.arch.regs.sp + 0x1F0 - 0x1B0

之后就是写内容,注意要按照结构体的顺序来写,sysnameversion中间的nodenamerelease要跳过不去动

1
2
3
4
5
6
7
def hook_uname_func(ql: Qiling,*args):
x0 = ql.arch.regs.sp + 0x1F0 - 0x1B0
ql.mem.write(x0,b'QilingOS\x00')
ql.mem.write(x0+65*3,b'ChallengeStart\x00') # (x0+65*3)跳过nodename和release

def challenge2(ql: Qiling):
ql.os.set_syscall("uname", hook_uname_func, QL_INTERCEPT.EXIT) # 函数返回时进行hook

Challenge 3: Make ‘/dev/urandom’ and ‘getrandom’ “collide”.

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
__int64 __fastcall challenge3(_BYTE *a1)
{
int v3; // [xsp+24h] [xbp+24h]
int i; // [xsp+28h] [xbp+28h]
int fd; // [xsp+2Ch] [xbp+2Ch]
char v6[8]; // [xsp+30h] [xbp+30h] BYREF
char buf[32]; // [xsp+38h] [xbp+38h] BYREF
char v8[32]; // [xsp+58h] [xbp+58h] BYREF
__int64 v9; // [xsp+78h] [xbp+78h]

fd = open("/dev/urandom", 0);
read(fd, buf, 0x20uLL);
read(fd, v6, 1uLL);
close(fd);
getrandom(v8, 32LL, 1LL);
v3 = 0;
for ( i = 0; i <= 31; ++i )
{
if ( buf[i] == v8[i] && buf[i] != v6[0] )
++v3;
}
if ( v3 == 32 )
*a1 = 1;
return v9 ^ _stack_chk_guard;
}

挑战三需要让open函数中获取的值与getrandom函数中的值一致,且/dev/urandom中的最后一个值需要与getrandom中的任意一个值不同

要同时实现两个需求,需要更改getrandom函数的返回值和文件的内容

首先使用ql.os.set_syscall来劫持getrandom函数,与上一个挑战不同,这次用到的ql.os.set_syscall最后传入的参数是QL_INTERCEPT.CALL,即在函数被调用时进行劫持,运行指定的函数而不运行原先的函数,这时,由于参数会相应地进行传入,因此要确保接受参数一致(有默认值除外)

1
2
3
def hook_getrandom_func(ql: Qiling,buf,buflen):
ql.mem.write(buf,b'\x00'*buflen)
ql.os.set_syscall_return(0)

之后要保证/dev/urandom内容一致,使用到自定义文件系统的功能,先定义一个FakeUrandom类,然后将内容填入read函数中,完成对read函数的重定向(close函数默认就行,fstat函数可以不要,注意读取单字节时要与读取多字节不同)

1
2
3
4
5
6
7
8
class FakeUrandom(QlFsMappedObject):
def read(self, size: int) -> bytes:
if size == 1:
return b'\xE9'
return b"\x00"*size

def close(self) -> int:
return 0

最终整合如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class FakeUrandom(QlFsMappedObject):
def read(self, size: int) -> bytes:
if size == 1:
return b'\xE9'
return b"\x00"*size

def close(self) -> int:
return 0

def hook_getrandom_func(ql: Qiling,buf,buflen):
ql.mem.write(buf,b'\x00'*buflen)
ql.os.set_syscall_return(0)

def challenge3(ql: Qiling):
ql.os.set_syscall("getrandom", hook_getrandom_func)
ql.add_fs_mapper("/dev/urandom", FakeUrandom())

Challenge 4: Enter inside the “forbidden” loop.

这个函数IDA无法解析,只剩下一个return 0

手动逆一下

1
2
3
4
5
6
7
8
9
void challenge4(_BYTE *v15){
int i = 0;
int j = 0;
while(i<j){
*v15 = 1;
i++;
}
return 0;
}

搞了一个进不去的循环,然后在里面进行判断变量的赋值,因此要进这个函数需要通过hook来改变寄存器内容进入这个函数,Qiling是有提供这个功能

这边需要hook跳转前的位置,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def hook_FE4(ql: Qiling):
'''
.text:0000000000000FE0 3F 00 00 6B CMP W1, W0
.text:0000000000000FE4 EB FE FF 54 B.LT loc_FC0
'''
ql.arch.regs.W0 = 1

def challenge4(ql: Qiling):
# 根据文件路径查找已经加载的文件,获取对应文件的基地址
# os.path.split(ql.path):os.path.split 函数将 ql.path 分成两部分:目录名和基本文件名。这个函数返回一个包含这两部分的元组。
# os.path.split(ql.path)[-1]:这将返回元组中的最后一个元素,即基本文件名。在 Python 中,-1 索引表示列表或元组的最后一个元素。
# base = ql.mem.get_lib_base(os.path.split(ql.path)[-1]):ql.mem.get_lib_base 函数使用提取的文件名作为参数,以获取已加载库的基地址。将返回的基地址赋值给变量 base
base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1]) # 需要先搞到基地址
enter_loop = base_addr + 0xfe0 # 基地址+偏移
ql.hook_address(hook_FE4,enter_loop)

Challenge 5: Guess every call to rand().

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
__int64 __fastcall challenge5(_BYTE *a1)
{
unsigned int v1; // w0
int i; // [xsp+20h] [xbp+20h]
int j; // [xsp+24h] [xbp+24h]
_DWORD v6[12]; // [xsp+28h] [xbp+28h]
__int64 v7; // [xsp+58h] [xbp+58h]

v1 = time(0LL);
srand(v1);
for ( i = 0; i <= 4; ++i )
{
v6[i] = 0;
v6[i + 6] = rand();
}
for ( j = 0; j <= 4; ++j )
{
if ( v6[j] != v6[j + 6] )
{
*a1 = 0;
return v7 ^ _stack_chk_guard;
}
}
*a1 = 1;
return v7 ^ _stack_chk_guard;
}

这边直接hook函数rand(),让其一直返回0即可

1
2
3
4
5
6
7
8
def hook_rand_func(ql: Qiling):
'''
返回值存在w0中
'''
ql.arch.regs.W0=0

def challenge5(ql:Qiling):
ql.os.set_api('rand',hook_rand_func, QL_INTERCEPT.CALL) # 不同于syscall,库函数要用set_api

由于challenge6卡死循环,所以没解决之前challenge5的内容不显示

Challenge 6: Avoid the infinite loop.

进去就是一个死循环,只能靠看汇编解决

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
.text:00000000000010F0                               ; =============== S U B R O U T I N E =======================================
.text:00000000000010F0
.text:00000000000010F0
.text:00000000000010F0 ; void challenge6()
.text:00000000000010F0 EXPORT challenge6
.text:00000000000010F0 challenge6 ; CODE XREF: start+1C0↓p
.text:00000000000010F0
.text:00000000000010F0 var_18= -0x18
.text:00000000000010F0 var_5= -5
.text:00000000000010F0 var_4= -4
.text:00000000000010F0
.text:00000000000010F0 ; __unwind {
.text:00000000000010F0 FF 83 00 D1 SUB SP, SP, #0x20
.text:00000000000010F4 E0 07 00 F9 STR X0, [SP,#0x20+var_18]
.text:00000000000010F8 FF 1F 00 B9 STR WZR, [SP,#0x20+var_4]
.text:00000000000010FC 20 00 80 52 MOV W0, #1
.text:0000000000001100 E0 6F 00 39 STRB W0, [SP,#0x20+var_5]
.text:0000000000001104 03 00 00 14 B loc_1110
.text:0000000000001104
.text:0000000000001108 ; ---------------------------------------------------------------------------
.text:0000000000001108
.text:0000000000001108 loc_1108 ; CODE XREF: challenge6+2C↓j
.text:0000000000001108 20 00 80 52 MOV W0, #1
.text:000000000000110C E0 1F 00 B9 STR W0, [SP,#0x20+var_4]
.text:000000000000110C
.text:0000000000001110
.text:0000000000001110 loc_1110 ; CODE XREF: challenge6+14↑j
.text:0000000000001110 E0 6F 40 39 LDRB W0, [SP,#0x20+var_5]
.text:0000000000001114 00 1C 00 12 AND W0, W0, #0xFF
.text:0000000000001118 1F 00 00 71 CMP W0, #0
.text:000000000000111C 61 FF FF 54 B.NE loc_1108
.text:000000000000111C
.text:0000000000001120 E0 07 40 F9 LDR X0, [SP,#0x20+var_18]
.text:0000000000001124 21 00 80 52 MOV W1, #1
.text:0000000000001128 01 00 00 39 STRB W1, [X0]
.text:000000000000112C 1F 20 03 D5 NOP
.text:0000000000001130 FF 83 00 91 ADD SP, SP, #0x20 ; ' '
.text:0000000000001134 C0 03 5F D6 RET
.text:0000000000001134 ; } // starts at 10F0
.text:0000000000001134
.text:0000000000001134 ; End of function challenge6

根据汇编可以知道,首先在0x10FC处将1存进了栈中,然后在0x1110处取了出来,AND了之后拿去和0做比较,比较不等则一直循环

由于hook操作是在指令执行前确定的,所以可以在0x1114处或0x1118处将W0改为0,处理方法和challenge4类似

1
2
3
4
5
6
7
8
9
10
11
12
13
def hook_1118(ql: Qiling):
'''
.text:0000000000001114 00 1C 00 12 AND W0, W0, #0xFF
.text:0000000000001118 1F 00 00 71 CMP W0, #0
.text:000000000000111C 61 FF FF 54 B.NE loc_1108
'''
ql.arch.regs.w0 = 0


def challenge6(ql: Qiling):
base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1]) # 需要先搞到基地址
stop_loop = base_addr + 0x1118 # 基地址+偏移
ql.hook_address(hook_1118,stop_loop)

此时challenge5的结果可以正常打印,但challenge6的没打印

Challenge 7: Don’t waste time waiting for ‘sleep’.

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

方法很多,此处列三种

1.将sleep过掉,直接返回

1
2
3
4
5
def hook_sleep_func(ql: Qiling):
return

def challenge7(ql: Qiling):
ql.os.set_api('sleep',hook_sleep_func)

2.在函数sleep调用前把w0改为0就行

1
2
3
4
5
6
7
def hook_1154(ql: Qiling):
ql.arch.regs.w0=0

def challenge7(ql: Qiling):
base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1]) # 需要先搞到基地址
hook_sleep = base_addr + 0x1154 # 基地址+偏移
ql.hook_address(hook_1154,hook_sleep)

3.将底层的nanosleep过掉

1
2
3
4
5
def hook_nanosleep_func(ql: Qiling):
return

def challenge7(ql: Qiling):
ql.os.set_syscall('nanosleep',hook_nanosleep_func)

总之舞台很大,而且challenge7过了之后后面的内容都能打印了

Challenge 8: Unpack the struct and write at the target address.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_DWORD *__fastcall challenge8(__int64 a1)
{
_DWORD *result; // x0
_DWORD *v3; // [xsp+28h] [xbp+28h]

v3 = malloc(0x18uLL); // 申请了一个0x18(24 Bytes)的空间
*(_QWORD *)v3 = malloc(0x1EuLL);// 申请了一个0x1E(30 Bytes)的空间,并将指向这个空间的指针存到v3的前8 Bytes中
v3[2] = 1337; // 将数1337存入v3的8~12 Bytes中
v3[3] = 1039980266; // 将数1039980266存入v3的12~16 Bytes中
strcpy(*(char **)v3, "Random data"); // 将字符串"Random data"存入申请的30 Bytes空间中
result = v3;
*((_QWORD *)v3 + 2) = a1; // 将a1(指针)存入v3最后的剩余空间(8 BYtes)中
return result;
}

根据伪代码与分析,可以知道结构体如下:

1
2
3
4
5
6
typedef struct{
char *str; // 8 字节的指针,指向字符串 "Random data" 所在的内存
uint32_t v1; // 4 字节的整数,值为 1337
uint32_t v2; // 4 字节的整数,值为 1039980266
int64_t check; // 8 字节的整数,值为传入的参数 a1
}

这边有两种方法可以完成挑战

1.在函数汇编的ret前可以找到结构体存储的位置,从而对结构体内容进行修改

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
def hook_11DC(ql: Qiling):
'''
.text:00000000000011D0 E0 17 40 F9 LDR X0, [SP,#0x30+var_8]
.text:00000000000011D4 E1 0F 40 F9 LDR X1, [SP,#0x30+var_18]
.text:00000000000011D8 01 08 00 F9 STR X1, [X0,#0x10]
.text:00000000000011DC 1F 20 03 D5 NOP
.text:00000000000011E0 FD 7B C3 A8 LDP X29, X30, [SP+0x30+var_30],#0x30
.text:00000000000011E4 C0 03 5F D6 RET
'''
heap_struct_addr = ql.unpack64(ql.mem.read(ql.arch.regs.sp + 0x28, 8)) # 获取堆中存结构体的地址
# 这边是根据返回值来确认堆中存结构体的地址的
# 因为返回的寄存器是X0,因此找到给X0赋值的地方就能找到指针了
# 或者可以用IDA的猜测功能,在反编译窗口选中result = v3处点tab键就可以定位执行这个操作的对应汇编地址
heap_struct = ql.mem.read(heap_struct_addr, 24) # dump堆内容
string_addr, magic, check_addr = struct.unpack('QQQ', heap_struct) # 解包结构体
ql.mem.write(check_addr, b"\x01") # 写内容

def challenge8(ql: Qiling):
base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1]) # 需要先搞到基地址
hook_struct = base_addr + 0x11DC # 基地址+偏移
ql.hook_address(hook_11DC,hook_struct)

2.使用Qiling提供的强大的search功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def hook_11DC(ql: Qiling):
MAGIC = 0x3DFCD6EA00000539 # 由于魔数分为两部分,为了搜索更精确,将其进行合并,搜索一个8 Bytes的大数
magic_addrs = ql.mem.search(ql.pack64(MAGIC))
for magic_addr in magic_addrs:
# 搜索出来的结构体可能不是需要的,因此要解包进行判断
candidate_heap_struct_addr = magic_addr - 8
candidate_heap_struct = ql.mem.read(candidate_heap_struct_addr, 24)
string_addr, _ , check_addr = struct.unpack('QQQ', candidate_heap_struct)

# 判断是否是正确的结构体
if ql.mem.string(string_addr) == "Random data":
# 正确则写一个字节在最后的指针里面
ql.mem.write(check_addr, b"\x01")
break


def challenge8(ql: Qiling):
base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1]) # 需要先搞到基地址
hook_struct = base_addr + 0x11DC # 基地址+偏移
ql.hook_address(hook_11DC,hook_struct)

Challenge 9: Fix some string operation to make the iMpOsSiBlE come true.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
__int64 __fastcall challenge9(bool *a1)
{
char *i; // [xsp+20h] [xbp+20h]
char dest[32]; // [xsp+28h] [xbp+28h] BYREF
char src[32]; // [xsp+48h] [xbp+48h] BYREF
__int64 v6; // [xsp+68h] [xbp+68h]

strcpy(src, "aBcdeFghiJKlMnopqRstuVWxYz");
src[27] = 0;
strcpy(dest, src);
for ( i = dest; *i; ++i )
*i = tolower((unsigned __int8)*i);
*a1 = strcmp(src, dest) == 0;
return v6 ^ _stack_chk_guard;
}

直接hooktolower函数就行

Challenge 10: Fake the ‘cmdline’ line file to return the right content.

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
__int64 __fastcall challenge10(_BYTE *a1)
{
int i; // [xsp+28h] [xbp+28h]
int fd; // [xsp+2Ch] [xbp+2Ch]
ssize_t v5; // [xsp+30h] [xbp+30h]
char buf[64]; // [xsp+38h] [xbp+38h] BYREF
__int64 v7; // [xsp+78h] [xbp+78h]

fd = open("/proc/self/cmdline", 0);
if ( fd != -1 )
{
v5 = read(fd, buf, 0x3FuLL);
if ( v5 > 0 )
{
close(fd);
for ( i = 0; v5 > i; ++i )
{
if ( !buf[i] )
buf[i] = 32;
}
buf[v5] = 0;
if ( !strcmp(buf, "qilinglab") )
*a1 = 1;
}
}
return v7 ^ _stack_chk_guard;
}

参考challenge3就可以了

1
2
3
4
5
6
7
8
9
class Fakecmdline(QlFsMappedObject):
def read(self, size: int) -> bytes:
return b"qilinglab"

def close(self) -> int:
return 0

def challenge10(ql: Qiling):
ql.add_fs_mapper("/proc/self/cmdline", Fakecmdline())

Challenge 11: Bypass CPUID/MIDR_EL1 checks.

这题X86/64aarch64的实现方式不同,都做一遍

首先是ARM64

1
2
3
4
5
6
7
8
9
10
11
12
__int64 __fastcall challenge11(_BYTE *a1)
{
__int64 result; // x0

result = 4919LL;
if ( _ReadStatusReg(ARM64_SYSREG(3, 0, 0, 0, 0)) >> 16 == 4919 )
{
result = (__int64)a1;
*a1 = 1;
}
return result;
}

这里用到了一个ARM架构的特殊寄存器midr_el1以及配套的汇编指令MRS,对应的函数为AArch64.SysRegRead

而这个midr_el1的使用例可以看这个

1
.text:00000000000013EC 00 00 38 D5                   MRS             X0, #0, c0, c0, #0

其作用是读取机器信息,这条完整的MRS指令每一位的含义如下

31 30 29 28 27 26 25 24 23 22 21 20 19 18 17 16 15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 0
1 1 0 1 0 1 0 1 0 0 1 1 o0 op1 op1 op1 CRn CRn CRn CRn CRm CRm CRm CRm op2 op2 op2 Rt Rt Rt Rt Rt

需要注意第十九位o0实际上是两位,只是高位都为1就省略掉了,即它的值为2或3

这条指令中的o0op1CRnCRmop2的组合用于指示从哪个寄存器中取值,而Rt表示取到哪个寄存器去

简单来说,指令MRS就是从midr_el1读取了信息然后存到了X0里面,然后就对X0的内容进行了判断

所以要过这关需要让X0的值变为指定内容

这里用到Qiling提供的hook_code函数

1
2
3
4
5
6
7
8
9
10
11
12
13
def midr_el1_hook(ql: Qiling, address, size):  
'''
.text:00000000000013EC 00 00 38 D5 MRS X0, #0, c0, c0, #0
'''
#hook_code函数每一条指令都会执行一次,因此要注意加判断条件
if ql.mem.read(address, size) == b"\x00\x00\x38\xD5":
# 直接改接收midr_el1值的寄存器
ql.arch.regs.X0 = 0x1337 << 16
# 别忘了直接跳过不让原来的指令执行(以字节为单位执行指令,所以这里要跳过4字节)
ql.arch.regs.arch_pc += 4

def challenge11(ql: Qiling):
ql.hook_code(midr_el1_hook)

但是hook_code函数每一条指令都会执行一次,这使得效率非常的低,因此考虑进行分段操作

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
def midr_el1_hook(ql: Qiling, address, size):  
'''
.text:00000000000013EC 00 00 38 D5 MRS X0, #0, c0, c0, #0
'''
#hook_code函数每一条指令都会执行一次,因此要注意加判断条件
if ql.mem.read(address, size) == b"\x00\x00\x38\xD5":
# 直接改接收midr_el1值的寄存器
ql.arch.regs.X0 = 0x1337 << 16
# 别忘了直接跳过不让原来的指令执行(以字节为单位执行指令,所以这里要跳过4字节)
ql.arch.regs.arch_pc += 4

def challenge11(ql: Qiling):
mem_map = ql.mem.map_info # 获取所有映射的信息
for entry in mem_map:
# entry内容为range start, range end, permissions mask, range label, is mmio
start, end, flags, label, _ = entry
# 取目标hook地址所在的映射区域
# [=] 555555554000 - 555555556000 r-x (5) [redacted]/qilinglab-aarch64
if os.path.split(ql.path)[-1] in label and flags == 5:
start_hook = start
end_hook = end
break

# 使用begin和end参数来指定hook的范围(采用base+偏移的方法实际上能缩得更短,不过都这样了为啥不用hook_address呢)
ql.hook_code(midr_el1_hook, begin=start_hook, end=end_hook)

当然也可以直接hook掉MRS所在的地址

1
2
3
4
5
6
7
8
9
10
def midr_el1_hook(ql: Qiling):
# 直接改接收midr_el1值的寄存器
ql.arch.regs.X0 = 0x1337 << 16
# 别忘了直接跳过不让原来的指令执行(以字节为单位执行指令,所以这里要跳过4字节)
ql.arch.regs.arch_pc += 4

def challenge11(ql: Qiling):
base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1])
MRS_instruction = base_addr+0x13EC
ql.hook_address(midr_el1_hook,MRS_instruction)

再看看X86/64

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

v12 = __readfsqword(0x28u);
_RAX = 0x40000000LL;
__asm { cpuid } // 获取cpu信息
v7 = _RCX;
v8 = _RDX;
if ( __PAIR64__(_RBX, _RCX) == 0x696C6951614C676ELL && (_DWORD)_RDX == 538976354 )
*a1 = 1;
sprintf(s,"%c%c%c%c",(unsigned int)_RBX,(unsigned int)((int)_RBX >> 8),(unsigned int)((int)_RBX >> 16),(unsigned int)((int)_RBX >> 24));
sprintf(v10,"%c%c%c%c",(unsigned int)v7,(unsigned int)(v7 >> 8),(unsigned int)(v7 >> 16),(unsigned int)(v7 >> 24));
sprintf(v11,"%c%c%c%c",(unsigned int)v8,(unsigned int)(v8 >> 8),(unsigned int)(v8 >> 16),(unsigned int)(v8 >> 24));
return __readfsqword(0x28u) ^ v12;
}

这里用到了一个cpuid的汇编指令,可以看看这个,介绍了不同功能码下cpuid返回的内容以及返回内容所在的寄存器

不过这里的功能吗是什么都不影响需要hook掉返回的值的操作

根据汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
.text:0000000000001191 89 D0                         mov     eax, edx
.text:0000000000001193 89 DE mov esi, ebx
.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

需要更改的寄存器有EDXEBXECX

可以使用hook_code

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.edx=0x20202062
ql.arch.regs.ebx=0x696C6951
ql.arch.regs.ecx=0x614C676E
ql.arch.regs.rip += 2 # 这里是直接劫持了cpuid,相当于取代cpuid这条指令为寄存器赋值

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

也可以用hook_address

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):
base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1])
cpuid_instruction = base_addr+0x1191 # 注意这里是在cpuid执行后再修改
ql.hook_address(hook_cpuid,cpuid_instruction)

Challenge Finished!

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
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
Challenge 7: SOLVED
Challenge 8: SOLVED
Challenge 9: SOLVED
Challenge 10: SOLVED
Challenge 11: SOLVED
You solved 11/11 of the challenges

整个代码文件如下

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
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
from qiling import *
from qiling.const import *
from qiling.os.mapper import QlFsMappedObject
import os
import struct

def challenge1(ql: Qiling):
ql.mem.map(0x1000,0x1000)
ql.mem.write(0x1337,ql.pack16(1337)) # pack16(value) == struct.pack('H', value)

################################################################################################################

def hook_uname_func(ql: Qiling,*args):
x0 = ql.arch.regs.sp + 0x1F0 - 0x1B0
ql.mem.write(x0,b'QilingOS\x00')
ql.mem.write(x0+65*3,b'ChallengeStart\x00') # (x0+65*3)跳过nodename和release

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

################################################################################################################

class FakeUrandom(QlFsMappedObject):
def read(self, size: int) -> bytes:
if size == 1:
return b'\xE9'
return b"\x00"*size

def close(self) -> int:
return 0

def hook_getrandom_func(ql: Qiling,buf,buflen):
ql.mem.write(buf,b'\x00'*buflen)
ql.os.set_syscall_return(0)

def challenge3(ql: Qiling):
ql.os.set_syscall("getrandom", hook_getrandom_func)
ql.add_fs_mapper("/dev/urandom", FakeUrandom())

################################################################################################################

def hook_FE4(ql: Qiling):
'''
.text:0000000000000FE0 3F 00 00 6B CMP W1, W0
.text:0000000000000FE4 EB FE FF 54 B.LT loc_FC0
'''
ql.arch.regs.W0 = 1

def challenge4(ql: Qiling):
# 根据文件路径查找已经加载的文件,获取对应文件的基地址
# os.path.split(ql.path):os.path.split 函数将 ql.path 分成两部分:目录名和基本文件名。这个函数返回一个包含这两部分的元组。
# os.path.split(ql.path)[-1]:这将返回元组中的最后一个元素,即基本文件名。在 Python 中,-1 索引表示列表或元组的最后一个元素。
# base = ql.mem.get_lib_base(os.path.split(ql.path)[-1]):ql.mem.get_lib_base 函数使用提取的文件名作为参数,以获取已加载库的基地址。将返回的基地址赋值给变量 base
base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1]) # 需要先搞到基地址
enter_loop = base_addr + 0xfe0 # 基地址+偏移
ql.hook_address(hook_FE4,enter_loop)

################################################################################################################

def hook_rand_func(ql: Qiling):
'''
返回值存在w0中
'''
ql.arch.regs.W0=0

def challenge5(ql:Qiling):
ql.os.set_api('rand',hook_rand_func, QL_INTERCEPT.CALL) # 不同于syscall,库函数要用set_api

################################################################################################################

def hook_1118(ql: Qiling):
'''
.text:0000000000001114 00 1C 00 12 AND W0, W0, #0xFF
.text:0000000000001118 1F 00 00 71 CMP W0, #0
.text:000000000000111C 61 FF FF 54 B.NE loc_1108
'''
ql.arch.regs.w0 = 0


def challenge6(ql: Qiling):
base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1]) # 需要先搞到基地址
stop_loop = base_addr + 0x1118 # 基地址+偏移
ql.hook_address(hook_1118,stop_loop)

################################################################################################################

# def hook_sleep_func(ql: Qiling):
# return

# def challenge7(ql: Qiling):
# ql.os.set_api('sleep',hook_sleep_func)

# def hook_1154(ql: Qiling):
# ql.arch.regs.w0=0

# def challenge7(ql: Qiling):
# base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1]) # 需要先搞到基地址
# hook_sleep = base_addr + 0x1154 # 基地址+偏移
# ql.hook_address(hook_1154,hook_sleep)

def hook_nanosleep_func(ql: Qiling):
return

def challenge7(ql: Qiling):
ql.os.set_syscall('nanosleep',hook_nanosleep_func)

################################################################################################################

# def hook_11DC(ql: Qiling):
# '''
# .text:00000000000011D0 E0 17 40 F9 LDR X0, [SP,#0x30+var_8]
# .text:00000000000011D4 E1 0F 40 F9 LDR X1, [SP,#0x30+var_18]
# .text:00000000000011D8 01 08 00 F9 STR X1, [X0,#0x10]
# .text:00000000000011DC 1F 20 03 D5 NOP
# .text:00000000000011E0 FD 7B C3 A8 LDP X29, X30, [SP+0x30+var_30],#0x30
# .text:00000000000011E4 C0 03 5F D6 RET
# '''
# heap_struct_addr = ql.unpack64(ql.mem.read(ql.arch.regs.sp + 0x28, 8)) # 获取堆中存结构体的地址
# # 这边是根据返回值来确认堆中存结构体的地址的
# # 因为返回的寄存器是X0,因此找到给X0赋值的地方就能找到指针了
# # 或者可以用IDA的猜测功能,在反编译窗口选中result = v3处点tab键就可以定位执行这个操作的对应汇编地址
# heap_struct = ql.mem.read(heap_struct_addr, 24) # dump堆内容
# string_addr, magic, check_addr = struct.unpack('QQQ', heap_struct) # 解包结构体
# ql.mem.write(check_addr, b"\x01") # 写内容

# def challenge8(ql: Qiling):
# base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1]) # 需要先搞到基地址
# hook_struct = base_addr + 0x11DC # 基地址+偏移
# ql.hook_address(hook_11DC,hook_struct)

def hook_11DC(ql: Qiling):
MAGIC = 0x3DFCD6EA00000539 # 由于魔数分为两部分,为了搜索更精确,将其进行合并,搜索一个8 Bytes的大数
magic_addrs = ql.mem.search(ql.pack64(MAGIC))
for magic_addr in magic_addrs:
# 搜索出来的结构体可能不是需要的,因此要解包进行判断
candidate_heap_struct_addr = magic_addr - 8
candidate_heap_struct = ql.mem.read(candidate_heap_struct_addr, 24)
string_addr, _ , check_addr = struct.unpack('QQQ', candidate_heap_struct)

# 判断是否是正确的结构体
if ql.mem.string(string_addr) == "Random data":
# 正确则写一个字节在最后的指针里面
ql.mem.write(check_addr, b"\x01")
break


def challenge8(ql: Qiling):
base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1]) # 需要先搞到基地址
hook_struct = base_addr + 0x11DC # 基地址+偏移
ql.hook_address(hook_11DC,hook_struct)

################################################################################################################

def hook_tolower_func(ql: Qiling):
return

def challenge9(ql: Qiling):
ql.os.set_api('tolower',hook_tolower_func)

################################################################################################################

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

def close(self) -> int:
return 0

def challenge10(ql: Qiling):
ql.add_fs_mapper("/proc/self/cmdline", Fakecmdline())

################################################################################################################

def midr_el1_hook(ql: Qiling, address, size):
'''
.text:00000000000013EC 00 00 38 D5 MRS X0, #0, c0, c0, #0
'''
#hook_code函数每一条指令都会执行一次,因此要注意加判断条件
if ql.mem.read(address, size) == b"\x00\x00\x38\xD5":
# 直接改接收midr_el1值的寄存器
ql.arch.regs.X0 = 0x1337 << 16
# 别忘了直接跳过不让原来的指令执行(以字节为单位执行指令,所以这里要跳过4字节)
ql.arch.regs.arch_pc += 4

# def challenge11(ql: Qiling):
# ql.hook_code(midr_el1_hook)

def challenge11(ql: Qiling):
mem_map = ql.mem.map_info # 获取所有映射的信息
for entry in mem_map:
# entry内容为range start, range end, permissions mask, range label, is mmio?
start, end, flags, label, _ = entry
# 取目标hook地址所在的映射区域
# [=] 555555554000 - 555555556000 r-x (5) [redacted]/qilinglab-aarch64
if os.path.split(ql.path)[-1] in label and flags == 5:
start_hook = start
end_hook = end
break

# 使用begin和end参数来指定hook的范围(采用base+偏移的方法实际上能缩得更短,不过都这样了为啥不用hook_address呢)
ql.hook_code(midr_el1_hook, begin=start_hook, end=end_hook)

# def midr_el1_hook(ql: Qiling):
# # 直接改接收midr_el1值的寄存器
# ql.arch.regs.X0 = 0x1337 << 16
# # 别忘了直接跳过不让原来的指令执行(以字节为单位执行指令,所以这里要跳过4字节)
# ql.arch.regs.arch_pc += 4

# def challenge11(ql: Qiling):
# base_addr = ql.mem.get_lib_base(os.path.split(ql.path)[-1])
# MRS_instruction = base_addr+0x13EC
# ql.hook_address(midr_el1_hook,MRS_instruction)

if __name__ == '__main__':
path = ['qilinglab/qilinglab-aarch64'] # 我们的目标
rootfs = "./examples/rootfs/arm64_linux" # 在你clone下来的仓库里
ql = Qiling(path, rootfs,verbose= QL_VERBOSE.OFF) # 关闭了VERBOSE,否则输出太难看了
challenge1(ql) # 在ql.run()之前,做好我们的hook工作
challenge2(ql)
challenge3(ql)
challenge4(ql)
challenge5(ql)
challenge6(ql)
challenge7(ql)
challenge8(ql)
challenge9(ql)
challenge10(ql)
challenge11(ql)
ql.run()