0x00 Qiling lab

通过qiling lab来学习qiling的使用,主要是一些hook技巧

https://www.shielder.com/blog/2021/07/qilinglab-release/

start

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# coding=UTF-8
from qiling import *
from qiling.const import QL_VERBOSE

def challenge1(ql: Qiling):
pass;

if __name__ == "__main__":
target=["qilinglab-aarch64"]
rootfs="/Users/kazamayc/tools/qiling/examples/rootfs/arm64_linux"
ql=Qiling(target,rootfs,verbose=QL_VERBOSE.OFF)
#ql.debugger = "gdb:127.0.0.1:9999"
challenge1(ql) # 在ql.run()之前,hook
ql.run()
1
target remote 172.30.2.232:9999

challenge1 内存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
_BYTE *__fastcall challenge1(_BYTE *a1)

_BYTE *result; // x0

result = (_BYTE *)*(unsigned int *)((char *)&loc_1334 + 3);
if ( result ) == 1337 )
{
result = a1;
*a1 = 1;
}
return result;
}
// 尝试读取0x1337的内存,如果0x1337上的值为1337就绕过check
// 我们的目的就是把这个内存上写上值
1
2
3
4
def challenge1(ql):
# 内存访问前必须被映射
ql.mem.map(0x1337//4096*4096, 4096, info = "[challenge1]")
ql.mem.write(0x1337, ql.pack16(1337))

可以在内存中查找或者写入数据

参考:

https://docs.qiling.io/en/latest/memory/#map-a-memory-area
https://docs.qiling.io/en/latest/struct/

challenge2 系统调用

读取uname,通关条件是

1
2
name.sysname=="QilingOS";
name.version=="ChallengeStart";
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
struct utsname
{
char sysname[65];
char nodename[65];
char release[65];
char version[65];
char machine[65];
char domainname[65];
};

__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) ) // 读取到值返回0
{
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
from qiling.const import QL_INTERCEPT
def my_uname(ql, pStcUname, *args):
ql.mem.write(pStcUname, b"QilingOS".ljust(65,b"\x00"))
ql.mem.write(pStcUname + 65*3, b'ChallengeStart'.ljust(65, b'\x00'))
return 0
#def my_uname(ql, *args):
#out_struct_addr = ql.arch.regs.sp + 0x40
#sysname_addr = out_struct_addr
#ql.mem.write(sysname_addr, b'QilingOS\x00')
#ql.mem.write(out_struct_addr + 65 * 3, b'ChallengeStart\x00')

def challenge2(ql):
# int uname(struct utsname *name);
# set_syscall:调用uname时,就会调用my_uname函数
# 系统调用除了使用名字,还可以使用系统调用号
# QL_INTERCEPT.EXIT:是退出系统调用后,再篡改返回值
ql.os.set_syscall('uname', my_uname, QL_INTERCEPT.EXIT)

pStcUname 是一个指向一个字符数组的指针,是uname的参数,该数组用于存储 uname 系统调用返回的操作系统信息,包括系统名称、网络名称、版本等

*args是在使用 Qiling 模拟系统调用时,需要将自定义的函数作为回调函数传递给 Qiling。在这种情况下, Qiling 框架将自动传递一些参数给自定义函数,例如系统调用号和系统调用参数。因此,在定义自定义函数时,为了避免这些参数引起错误,通常会使用可变长度参数 *args 来捕获和忽略这些参数。

还可以使用QL_INTERCEPT.CALL等对系统调用函数进行替代。

参考:

https://man7.org/linux/man-pages/man3/uname.3p.html

https://docs.qiling.io/en/latest/hijack/

challenge3 文件系统

1
2
/dev/urandom == getrandom
且一个字节的随机数和其他的随机数都不一样
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;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
from qiling.os.mapper import QlFsMappedObject
class Fake_urandom(QlFsMappedObject):
# 使用QlFsMappedObject来创建自定义文件系统,最少要实现close
def read(self, size):
if(size == 1):
return b"\x02"
else:
return b"\x01" * size
def close(self):
return 0

def fake_getrandom(ql, buf, buflen, flags, *args):
ql.mem.write(buf, b"\x01"*buflen)
return 0

def challenge3(ql):
ql.add_fs_mapper("/dev/urandom", Fake_urandom())
# 将urandom文件映射到Fake_urandom上,当程序读取urandom时会映射到Fake_urandom上
# 将虚拟路径映射到用户定义的文件类型,该对象允许对交互进行更精细的控制
ql.os.set_syscall('getrandom', fake_getrandom, QL_INTERCEPT.EXIT)

challenge4 地址和寄存器

1
2
3
4
5
6
7
void challenge4(check) {
int i = 0;
while (i < 0) {
check = 1;
i++;
}
}

b.lt 小于时跳转,所以设置W0的值为1就可以

image-20230331135533174

1
2
3
4
5
6
7
8
def loop_hook(ql):
ql.arch.regs.write("w0", 0x1)

def challenge4(ql):
base_addr = ql.mem.get_lib_base(ql.path)
# 函数会尝试将此路径中的文件加载到 Qiling 的内存空间中,并返回其基地址。
loop_enter = base_addr+0xFE0
ql.hook_address(loop_hook, loop_enter)

参考:

https://docs.qiling.io/en/latest/hook/

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
__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;
}

rand是库函数,不是系统调用,所以不能使用set_syscall,只能用set_api

我们只要让rand的返回值为0就可以了

使用set_api来 hook 函数的返回值时,不能使用 return 语句来修改返回值,必须要改寄存器

1
2
3
4
5
def rand_hook(ql, *args):
# x0是返回结果
ql.arch.regs.x0 = 0
def challenge5(ql):
ql.os.set_api("rand", rand_hook)

challenge6 地址

image-20230403114144274

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

def challenge6(ql):
base_addr = ql.mem.get_lib_base(ql.path)
loop_addr = base_addr + 0x1114
ql.hook_address(loop_bypass_hook, loop_addr)

challenge7 外部函数

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

hook sleep函数有多种方法

  1. 用set_api,用一个空函数替换sleep

  2. 用set_api,把参数改了

  3. 用set_syscall,将nanosleep改了

1
2
3
4
5
6
7
8
9
10
11
12
13
def fake_sleep1(ql, *args):
return

def fake_sleep2(ql, *args):
ql.arch.regs.write("w0", 0)

def hook_nanosleep(ql, *args):
return 0

def challenge7(ql):
#ql.os.set_api("sleep", fake_sleep1)
#ql.os.set_api("sleep", fake_sleep2, QL_INTERCEPT.ENTER)
ql.os.set_syscall('nanosleep', hook_nanosleep)

challenge8 结构体

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);
*(_QWORD *)v3 = malloc(0x1EuLL);
v3[2] = 0x539;
v3[3] = 0x3DFCD6EA;
strcpy(*(char **)v3, "Random data");
result = v3;
*((_QWORD *)v3 + 2) = a1;
return result;
}

是让我们去寻找一个类似的结构体

1
2
3
4
5
struct something(0x18){ 
string_ptr -> malloc (0x1e) -> "Random data"
long_int = 0x3DFCD6EA00000539
check_addr -> check;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import struct
def search_heap1(ql):
#从内存中搜索字符串
nMagic = 0x3DFCD6EA00000539
pMagics = ql.mem.search(ql.pack64(nMagic))
#内存可能出现了几次字符串,使用字符串“Random data”验证是否找到了正确的数据
for pMagic in pMagics:
pHeap1 = pMagic - 8
heap1 = ql.mem.read(pHeap1, 24)
pHeap2, _, pFlag = struct.unpack("QQQ", heap1)
#比较地址和读到的字符串
if ql.mem.string(pHeap2) == "Random data":
#找到结构体的位置然后写入1
ql.mem.write(pFlag, b"\x01")
break

def challenge8(ql):
base_addr = ql.mem.get_lib_base(ql.path)
ql.hook_address(search_heap1, base_addr + 0x11DC) # 0x11DC : nop

hook一个具体地址。执行指定地址时将调用已注册的回调。

参考链接:

https://docs.qiling.io/en/latest/memory/

https://docs.qiling.io/en/latest/hook/

还有第二种方法,可以直接读取栈上的值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def search_heap2(ql):
'''
001011d0 e0 17 40 f9 ldr x0,[sp, #0x28] <---- heap structure on stack
001011d4 e1 0f 40 f9 ldr x1,[sp, #0x18]
001011d8 01 08 00 f9 str x1,[x0, #0x10]
001011dc 1f 20 03 d5 nop <----------------------- HOOK HERE
001011e0 fd 7b c3 a8 ldp x29=>local_30,x30,[sp], #0x30
001011e4 c0 03 5f d6 ret

'''
# Get heap structure address
heap_struct_addr = ql.unpack64(ql.mem.read(ql.arch.regs.sp + 0x28, 8))

# Dump and unpack structure
heap_struct = ql.mem.read(heap_struct_addr, 24)
some_string_addr, magic, check_addr = struct.unpack('QQQ', heap_struct)

# Write 1 to check
ql.mem.write(check_addr, b"\x01")

def challenge8(ql):
base_addr = ql.mem.get_lib_base(ql.path)
ql.hook_address(search_heap2, base_addr + 0x11DC) # 0x11DC : nop

challenge9 外部函数

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

本题主要原理是在tolower把字符串转换成小写之前,使其失效即可。

1
2
3
4
5
def tolower_hook(ql):
return

def challenge9(ql):
ql.os.set_api("tolower", tolower_hook)

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
__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;
}

一种方法:hook文件系统

1
2
3
4
5
6
7
8
9
10
class Fake_cmdline(QlFsMappedObject):
def read(self, size):
return b"qilinglab"
def fstat(self):
return -1
def close(self):
return 0

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

第二种方法:本地创建文本

1
$ echo -n "qilinglab" > fake_cmdline
1
2
def challenge10(ql):
ql.add_fs_mapper("/proc/self/cmdline", "./fake_cmdline")

第三种方法:无需编写任何代码

1
2
$ mkdir -p ./my_rootfs/proc/self
$ echo -n "qilinglab" > my_rootfs/proc/self/cmdline

但是上述方法我都没执行成功,于是我直接劫持了strcmp

1
2
3
4
def strcmp_hook(ql):
ql.arch.regs.x0 = 0
def challenge10(ql):
ql.os.set_api("strcmp",strcmp_hook)

challenge11 CPU指令

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

image-20230404155607139

其实可以直接hook 0x1400的x1为0x1337

1
2
3
4
5
def fake_end(ql):
ql.arch.regs.write("x1", 0x1337)
def challenge11(ql):
base_addr = ql.mem.get_lib_base(ql.path)
ql.hook_address(fake_end, base_addr + 0x1400)

或者hook指令

1
2
3
4
5
6
7
8
9
10
11
def fake_end2(ql, address, size):
'''
000013ec 00 00 38 d5 mrs x0,midr_el1
'''
if ql.mem.read(address, size) == b"\x00\x00\x38\xD5":
# Write the expected value to x0
ql.arch.regs.x0 = 0x1337 << 16
# Go to next instruction
ql.arch.regs.arch_pc += 4
def challenge11(ql):
ql.hook_code(fake_end2)