浅尝Qiling

前言

这篇文章也是好几个月之前写的了,当时因为忙了其他事情,一直没有整理发布,刚才无意中看到了以前的草稿,就索性发一下。Qiling 这个框架对于模拟运行二进制程序时的 hook 非常方便,可以很细致的获取或修改某个时刻的内存值,学习了该框架,可以后续在此基础上开发一些小工具。

其实网上关于 qiling-lab 资料已经非常多了,自学起来已经没有什么难度。本文也是照猫画虎完成了 qiling-lab 的 11 个挑战,并没有做其他的延伸学习,更多的记录是自己在学习过程时踩过的坑和一些思考。

qiling安装

pip3 install qiling

一条命令直接安装,不过我在 ubuntu18.04 中遇到了一个报错

image-20240507081935848

ERROR: Could not find a version that satisfies the requirement qiling (from versions: none)
ERROR: No matching distribution found for qiling

表示没有找到 qiling 的库,结合 log 可知是从 https://pypi.douban.com/simple/ 进行搜索的。

使用 pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple qiling 指定从清华源中重新获取,在 ubuntu18.04 上正常安装

image-20240507082508530

上面是用 -i 临时指定的,用下面的命令可以将配置永久生效

pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

执行下面的命令把 qiling 的仓库给 clone 下来

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

有报错如下:

➜  Desktop git clone https://github.com/qilingframework/qiling.git --recursiv
Cloning into 'qiling'...
fatal: unable to access 'https://github.com/qilingframework/qiling.git/': Failed to connect to 127.0.0.1 port 1087: Connection refused

查看 git 的配置

cat ~/.gitconfig 

输出如下

[http]
proxy = http://127.0.0.1:1087

这说明之前本机的代理留的有问题,接下来我可以调整配置文件,可以将文件里面关于代理的配置给删除,不走代理。将其走我宿主机的代理,比如我改成下面这个样子。

[http]
proxy = http://192.168.144.154:4780

再执行 git clone https://github.com/qilingframework/qiling.git --recursiv 命令,安装成功

Cloning into 'qiling'...
remote: Enumerating objects: 47312, done.
remote: Counting objects: 100% (952/952), done.
remote: Compressing objects: 100% (387/387), done.
remote: Total 47312 (delta 596), reused 837 (delta 561), pack-reused 46360
Receiving objects: 100% (47312/47312), 67.78 MiB | 4.98 MiB/s, done.
Resolving deltas: 100% (35960/35960), done.
Submodule 'examples/rootfs' (https://github.com/qilingframework/rootfs.git) registered for path 'examples/rootfs'
Cloning into '/home/zikh/Desktop/qiling/examples/rootfs'...
remote: Enumerating objects: 1253, done.
remote: Counting objects: 100% (1253/1253), done.
remote: Compressing objects: 100% (795/795), done.
remote: Total 1253 (delta 344), reused 1249 (delta 342), pack-reused 0
Receiving objects: 100% (1253/1253), 160.85 MiB | 3.01 MiB/s, done.
Resolving deltas: 100% (344/344), done.
Submodule path 'examples/rootfs': checked out '32c4fcf52f4aa0efaa1cb03ab6b2186c61f512c6'

安装 Qdb

git clone git@github.com:ucgJhe/Qdb.git

报错如下:

Cloning into 'Qdb'...
git@github.com: Permission denied (publickey).
fatal: Could not read from remote repository.

Please make sure you have the correct access rights
and the repository exists.

可能是缺少公钥,或者权限不足。可以用 git clone https://github.com/ucgJhe/Qdb.git 命令,以 HTTPS 协议进行克隆,不需要 SSH 密钥认证。

➜  Desktop git clone https://github.com/ucgJhe/Qdb.git

Cloning into 'Qdb'...
remote: Enumerating objects: 289, done.
remote: Counting objects: 100% (289/289), done.
remote: Compressing objects: 100% (194/194), done.
remote: Total 289 (delta 153), reused 221 (delta 91), pack-reused 0
Receiving objects: 100% (289/289), 403.12 KiB | 686.00 KiB/s, done.
Resolving deltas: 100% (153/153), done.

challenge

challenge1

_BYTE *__fastcall challenge1(_BYTE *a1)
{
_BYTE *result; // rax

result = (_BYTE *)MEMORY[0x1337];
if ( MEMORY[0x1337] == 1337 )
{
result = a1;
*a1 = 1;
}
return result;
}

需要让内存 0x1337 处的值为 1337 。就正常是映射内存,然后用 write 去向内存里写值,之后还会遇到很多次,常规操作。需要注意的是 ql.mem.write 的第二个参数是 bytes 类型 ,不然就报错,这里用 ql.pack16 打包,或者直接写成 b"\x39\x05" 也是可以的

def challenge1(ql: Qiling):
ql.mem.map(0x1000, 0x1000)
ql.mem.write(0x1337, ql.pack16(1337))
#ql.mem.write(0x1337, b"\x39\x05")

Pack and Unpack - Qiling Framework Documentation

challenge2

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[16]; // [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 函数执行完,使其参数 sysnameversion 解析成指定的字符串。这个需要用到 ql.os.set_syscall 从系统调用层面进行 hook ,通过解析出 uname 执行时的第一个参数(也就是结构体地址),去向结构体中的指定字段写入指定字符串即可

def my_syscall_uname(ql: Qiling, *args) -> int:
"""
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: Qiling):
ql.os.set_syscall("uname", my_syscall_uname, QL_INTERCEPT.EXIT)

劫持 - 麒麟框架文档 — Hijack - Qiling Framework Documentation

challenge3

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 文件中读取 0x20 字节的随机数 ,再用 getrandom 函数生成 0x20 字节的随机数,两组随机数一样算作通关。

先使用 ql.os.set_syscallgetrandom 系统调用进行 hook 控制其返回值,再用 QlFsMappedObject 自定义文件系统,我的理解就是创建出一个类,类中的函数是模拟对指定文件的操作。比如在该类中定义一个 read函数,就是当有操作对这个文件进行读的时候,调用read函数,返回读取的数据。

class Fake_urandom(QlFsMappedObject):
def read(self, size):
if size == 0x20:
return b'\xff'*size
if size == 1:
return b'\x01'

def fstat(self):
return -1
def close(self):
return 0

def challenge3(ql: Qiling):
ql.add_fs_mapper("/dev/urandom", Fake_urandom())
ql.os.set_syscall("getrandom", my_syscall_getrandom, QL_INTERCEPT.EXIT)


def my_syscall_getrandom(ql, buf, buflen, flags, *args):
ql.mem.write(buf, b"\xff"*buflen)
return buflen

Hijack - Qiling Framework Documentation

challenge4

直接用 IDA 看伪代码的话,是 return 0

__int64 challenge4()
{
return 0LL;
}

看汇编发现是有个循环,从 0xE33jmp 进入循环

.text:0000000000000E1D ; __unwind {
.text:0000000000000E1D push rbp
.text:0000000000000E1E mov rbp, rsp
.text:0000000000000E21 mov [rbp+var_18], rdi
.text:0000000000000E25 mov [rbp+var_8], 0
.text:0000000000000E2C mov [rbp+var_4], 0
.text:0000000000000E33 jmp short loc_E40
.text:0000000000000E35 ; ---------------------------------------------------------------------------
.text:0000000000000E35
.text:0000000000000E35 loc_E35: ; CODE XREF: challenge4+29↓j
.text:0000000000000E35 mov rax, [rbp+var_18]
.text:0000000000000E39 mov byte ptr [rax], 1
.text:0000000000000E3C add [rbp+var_4], 1
.text:0000000000000E40
.text:0000000000000E40 loc_E40: ; CODE XREF: challenge4+16↑j
.text:0000000000000E40 mov eax, [rbp+var_8]
.text:0000000000000E43 cmp [rbp+var_4], eax
.text:0000000000000E46 jl short loc_E35
.text:0000000000000E48 nop
.text:0000000000000E49 pop rbp
.text:0000000000000E4A retn
.text:0000000000000E4A ; } // starts at E1D

先执行 mov eax, [rbp+var_8] cmp [rbp+var_4], eax jl short loc_E35 三条指令。因为 [rbp+var_8][rbp+var_4]初始值就是 0,而 jl 汇编指令表示源操作数小于目标操作数时进行跳转,此时 cmp 比较的结果是二者相等。因此不会触发 jl 的跳转,直接将函数返回。如果想通过本次挑战的 check ,是需要跳转到 0xE35 执行 mov rax, [rbp+var_18]mov byte ptr [rax], 1 两个指令。

采用 hook_address 函数对一个特定的地址进行 hook ,当执行到这个地址时可以触发指定的回调函数。用回调函数修改此时 eax 寄存器的值为 1

def change_eax_func(ql: Qiling):
ql.arch.regs.write("eax", 1)

def challenge4(ql: Qiling):
base_addr = ql.mem.get_lib_base(ql.path)
ql.hook_address(change_eax_func, base_addr + 0xe43)

这样可以让 jl 指令实现跳转,从 0xE35 开始往下执行,触发关键代码。从而之后通过 check,再往下执行时 jl 不会跳转,该函数返回。

不知道为什么从第五关开始,即使通关了,也不会立刻输出 Challenge 5: SOLVED (搞的我以为没通关呢),只有下一关通过了,才能看到上一关的状态。我想了一个能够看本关是否通过的方法,可以 hook 本关给通关标识赋值的代码。这样当本关触发获胜条件时,可以执行设置的回调函数,让回调函数输出一句话就知道本关是否过了,就像下面这样。

ql.hook_address(win, base_addr + 0xEDD) 
def win(ql: Qiling):
print('[*] win')

Register - Qiling Framework Documentation

Hook - Qiling Framework Documentation

challenge5

第五关的目的是让 rand 函数执行五次,每次的返回值全部都是 0

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

这个思路肯定是用 ql.os.set_api 来劫持库函数,但我最开始以为可以这样写

def challenge5(ql: Qiling):
base_addr = ql.mem.get_lib_base(ql.path)
ql.hook_address(win, base_addr + 0xEDD)
ql.os.set_api('rand', my_rand)

def my_rand(ql, *args):
return 0

因为我想着通过劫持 rand 函数,并在自定义的函数中直接返回 0,从而实现让 rand 函数返回 0 的目的。测试发现这样控制不了库函数 rand返回值为 0

正确做法是用 ql.arch.regs.write("rax",0) 代码来修改函数的 rax 寄存器,从而实现劫持 rand 函数的返回值。

def challenge5(ql: Qiling):
base_addr = ql.mem.get_lib_base(ql.path)
ql.hook_address(win, base_addr + 0xEDD)
ql.os.set_api('rand', my_rand)

def my_rand(ql, *args):
ql.arch.regs.write("rax",0)

challenge6

直接看伪代码的话发现是个死循环

void challenge6()
{
while ( 1 )
;
}

执行流会先通过 jmp short loc_F12 指令跳转到 0xF12 地址处,将 1 赋值给 eax 寄存器,经过 test al, al 指令后(eax1)将 ZF 标志位寄存器清零。而 jnz 指令指的是 ZF 寄存器为 0 时跳转, [rbp+var_5] 的内容一直为 1 ,从而陷入了一个死循环。

PS:这里记一下 jnz 指令,这个指令本身的全称是 jump if not zero ,翻译过来虽然是不为零时跳转,但这个不为零指的并不是 ZF 寄存器的值,而是运算结果( cmp 让两个操作数相减的结果 )。运算结果和 ZF 寄存器的值刚好是相反的关系,因此 ZF==0jnz 才跳转

.text:0000000000000EF6                 push    rbp
.text:0000000000000EF7 mov rbp, rsp
.text:0000000000000EFA mov [rbp+var_18], rdi
.text:0000000000000EFE mov [rbp+var_4], 0
.text:0000000000000F05 mov [rbp+var_5], 1
.text:0000000000000F09 jmp short loc_F12
.text:0000000000000F0B ; ---------------------------------------------------------------------------
.text:0000000000000F0B
.text:0000000000000F0B loc_F0B: ; CODE XREF: challenge6+22↓j
.text:0000000000000F0B mov [rbp+var_4], 1
.text:0000000000000F12
.text:0000000000000F12 loc_F12: ; CODE XREF: challenge6+13↑j
.text:0000000000000F12 movzx eax, [rbp+var_5]
.text:0000000000000F16 test al, al
.text:0000000000000F18 jnz short loc_F0B
.text:0000000000000F1A mov rax, [rbp+var_18]
.text:0000000000000F1E mov byte ptr [rax], 1
.text:0000000000000F21 nop
.text:0000000000000F22 pop rbp
.text:0000000000000F23 retn

解决方式和 challenge4 类似,在地址 0XF16 处进行 hook ,强行修改寄存器 eax 的值为 0 。这样 jnz 就不会再跳转,从而执行 mov byte ptr [rax], 1 通过本次跳转。

challenge7

这个挑战中是直接给了通关的标志,但是要休眠 0xFFFFFFFF 秒。

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

我能想到两种简单的思路分别是改 sleep 的参数或者直接把 sleep 函数给 hook 掉,经过测试,这两种都可以实现

def change_edi(ql: Qiling):
ql.arch.regs.write("rdi", 0)

def my_sleep(ql: Qiling):
return

def challenge7(ql: Qiling):
# base = ql.mem.get_lib_base(ql.path)
# ql.hook_address(change_edi, base+0xF3c)
ql.os.set_api('sleep', my_sleep)

然后看了其他师傅的文章,这里也可以用 ql.os.set_syscall 劫持 sleep 库函数中的系统调用 nanosleep

challenge8

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

挑战8最开始让我感到比较困惑,因为前面几关都有明显通关的标识,但是这个代码也没有发现任何自己可以去改变通关标识的代码。我以为是直接让去定位通关的标识,强行修改内存的值为 1

于是写了一个很作弊的代码😶‍🌫️,这个可以过每一关。就因为知道通关的标识位于 rbp-0x18 ,那直接用 ql.mem.write 就把内存给改了… 但是我写完想了一下,这也没意义,没体现出来这一关的特别

def func(ql: Qiling):
rbp_value=ql.arch.regs.read("rbp")
rbp_value=ql.unpack(ql.mem.read(rbp_value-0x18,0x8))
ql.mem.write(rbp_value,b"\x01")
print("[*] debug",hex(rbp_value))

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

于是看了一下其他师傅的 wp ,才知道这关的本质是要拿到堆里面存放的通关标识地址(也就是a1)。重新再看一下这个代码,首先程序申请了一个堆块,用来存储创建的结构体

_DWORD *__fastcall challenge8(__int64 a1, __m128 _XMM0)
{
_DWORD *result; // rax
_DWORD *v3; // [rsp+18h] [rbp-8h]

v3 = malloc(0x18uLL);
*v3 = malloc(0x1EuLL);
v3[2] = 0x539;
v3[3] = 0x3DFCD6EA;
strcpy(*v3, "Random data");
result = v3;
*(v3 + 2) = a1;
return result;
}

然后在第一个成员中存放了八字节的指针,指向了另一个堆块。第二个成员中存储了 0x3DFCD6EA00000539 数据,第三个成员中存放了通关标识的地址。

在第一个成员指向的堆块中写入了字符串 Random data 。这一关考察是通过搜索内存中的魔数,快速定位指定内存。再进行特征匹配,精准的从内存中取出想要的值。我这里是先用 ql.mem.search 匹配魔数,定位第一个堆块的地址,将其减八存放的就是第二次 malloc 出内存的地址,用 ql.mem.read 可以把 Random data 字符串给打印出来。打印出 Random data 字符串,就能确定找的地址没问题了,用 ql.mem.write 向第三个指针(通关标识的地址)中写入 \x01 ,本题通关。

def search_mem(ql: Qiling):
MAGIC=0x3DFCD6EA00000539
struct_address = ql.mem.search(ql.pack64(MAGIC))

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("[*] debug2",hex(mem_value3))

print(ql.mem.read(mem_value1, 0x10))
ql.mem.write(mem_value3, b"\x01")


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

challenge9

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

这一关会将 aBcdeFghiJKlMnopqRstuVWxYz 变成全小写将其比较。我最开始的思路是劫持 tolower 函数让 rdi 寄存器的值给 rax 就结束,就像下面这样

def my_tolower(ql: Qiling,*args):
ql.arch.regs.write("rax", ql.arch.regs.read("rdi"))

这样写这关也能过,之后看了其他师傅的 wp 才注意到 srcdest 开始全被写成字符串 aBcdeFghiJKlMnopqRstuVWxYz 了。因此可以 hook tolower 函数,让其直接 return 。后续 strcmp 函数做比较时自然就通过了。

challenge10

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

程序最开始读取 /proc/self/cmdline 文件的内容存取到 buf 数组中,然后 buf 数组再全部置空,让 buf 和字符串 qilinglab 做比较,如果相同则通关。

hook 一下 strcmp 函数的返回值即可

def my_strcmp(ql: Qiling):
ql.arch.regs.write("rax", 0)

def challenge10(ql: Qiling):
ql.os.set_api("strcmp", my_strcmp)

challenge11

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 }
v7 = _RCX;
v8 = _RDX;
if ( __PAIR64__(_RBX, _RCX) == 0x696C6951614C676ELL && _RDX == 0x20202062 )
*a1 = 1;
sprintf(s, "%c%c%c%c", _RBX, (_RBX >> 8), (_RBX >> 16), (_RBX >> 24));
sprintf(v10, "%c%c%c%c", v7, (v7 >> 8), (v7 >> 16), (v7 >> 24));
sprintf(v11, "%c%c%c%c", v8, (v8 >> 8), (v8 >> 16), (v8 >> 24));
return __readfsqword(0x28u) ^ v12;
}

直接看伪代码,说实话 if 这里没看懂,这里看汇编更清楚

.text:0000555555555195                 mov     [rbp+var_30], esi
.text:0000555555555198 mov [rbp+var_34], ecx
.text:000055555555519B mov [rbp+var_2C], eax
.text:000055555555519E cmp [rbp+var_30], 696C6951h
.text:00005555555551A5 jnz short loc_5555555551C0
.text:00005555555551A7 cmp [rbp+var_34], 614C676Eh
.text:00005555555551AE jnz short loc_5555555551C0
.text:00005555555551B0 cmp [rbp+var_2C], 20202062h
.text:00005555555551B7 jnz short loc_5555555551C0
.text:00005555555551B9 mov rax, [rbp+var_48]
.text:00005555555551BD mov byte ptr [rax], 1
.text:00005555555551C0
.text:00005555555551C0 loc_5555555551C0: ; CODE XREF: challenge11+4C↑j
.text:00005555555551C0 ; challenge11+55↑j ...
.text:00005555555551C0 mov eax, [rbp+var_30]
.text:00005555555551C3 sar eax, 18h
.text:00005555555551C6 mov edi, eax
.text:00005555555551C8 mov eax, [rbp+var_30]
.text:00005555555551CB sar eax, 10h
.text:00005555555551CE mov esi, eax
.text:00005555555551D0 mov eax, [rbp+var_30]
.text:00005555555551D3 sar eax, 8
.text:00005555555551D6 mov ecx, eax
.text:00005555555551D8 mov edx, [rbp+var_30]
.text:00005555555551DB lea rax, [rbp+s]
.text:00005555555551DF mov r9d, edi
.text:00005555555551E2 mov r8d, esi
.text:00005555555551E5 lea rsi, format ; "%c%c%c%c"
.text:00005555555551EC mov rdi, rax ; s
.text:00005555555551EF mov eax, 0
.text:00005555555551F4 call _sprintf

直接关注 cmp jnz 指令部分,也就是下面的核心部分

.text:0000555555555195                 mov     [rbp+var_30], esi
.text:0000555555555198 mov [rbp+var_34], ecx
.text:000055555555519B mov [rbp+var_2C], eax
.text:000055555555519E cmp [rbp+var_30], 696C6951h
.text:00005555555551A5 jnz short loc_5555555551C0
.text:00005555555551A7 cmp [rbp+var_34], 614C676Eh
.text:00005555555551AE jnz short loc_5555555551C0
.text:00005555555551B0 cmp [rbp+var_2C], 20202062h
.text:00005555555551B7 jnz short loc_5555555551C0

其实就是在比较 esi ecx eax 三个寄存器的值。只要有一个不同,就会触发 jnz 跳转。

继续往下审计,当 jnz 都不跳转时会执行到,这个 rbp+var_48 就是通关标识的地址,这里就相当于标识本关通过。

.text:00005555555551B9                 mov     rax, [rbp+var_48]
.text:00005555555551BD mov byte ptr [rax], 1

本题的思路就是 hook 0x1195 地址,去设置下 esi ecx eax 三个寄存器的值就可以了。

def change_regs(qi: Qiling):
ql.arch.regs.write("esi", 0x696C6951)
ql.arch.regs.write("ecx", 0x614C676E)
ql.arch.regs.write("eax", 0x20202062)

def challenge11(ql: Qiling):
base = ql.mem.get_lib_base(ql.path)
ql.hook_address(change_regs, base + 0x1195)

EXP

from qiling import *
from qiling.const import QL_INTERCEPT
from qiling.const import QL_VERBOSE
from qiling.os.mapper import QlFsMappedObject
import os
import struct
import time

def challenge1(ql: Qiling):
ql.mem.map(0x1000, 0x1000)
# ql.mem.write(0x1337, ql.pack16(1337))
ql.mem.write(0x1337, b"\x39\x05")
def challenge2(ql: Qiling):
ql.os.set_syscall("uname", my_syscall_uname, QL_INTERCEPT.EXIT)

class Fake_urandom(QlFsMappedObject):
def read(self, size):
if size == 0x20:
return b'\xff'*size
if size == 1:
return b'\x01'

def fstat(self):
return -1
def close(self):
return 0

def challenge3(ql: Qiling):
ql.add_fs_mapper("/dev/urandom", Fake_urandom())
ql.os.set_syscall("getrandom", my_syscall_getrandom, QL_INTERCEPT.EXIT)


def my_syscall_getrandom(ql, buf, buflen, flags, *args):
ql.mem.write(buf, b"\xff"*buflen)
return buflen

def challenge4(ql: Qiling):
base_addr = ql.mem.get_lib_base(ql.path)
ql.hook_address(change_eax_func, base_addr + 0xe43)

def challenge5(ql: Qiling):
base_addr = ql.mem.get_lib_base(ql.path)
ql.hook_address(win, base_addr + 0xEDD)
ql.os.set_api('rand', my_rand)

def my_rand(ql, *args):
ql.arch.regs.write("rax",0)

def debug_func1(ql: Qiling):
print("debug_test ==> ",ql.arch.regs.read("rax"))

def hook_cmp_rax(ql:Qiling):
ql.arch.regs.write("rax", 0x0)

def change_edi(ql: Qiling):
ql.arch.regs.write("rdi", 0)

def my_sleep(ql: Qiling):
time.sleep(0)
return

def my_nanosleep(ql: Qiling):
return


def func(ql: Qiling):

rbp_value=ql.arch.regs.read("rbp")
rbp_value=ql.unpack(ql.mem.read(rbp_value-0x18,0x8))
# print(rbp_value)
ql.mem.write(rbp_value,b"\x01")
print("[*] debug",hex(rbp_value))

def search_mem(ql: Qiling):
MAGIC=0x3DFCD6EA00000539
struct_address = ql.mem.search(ql.pack64(MAGIC))

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("[*] debug2",hex(mem_value3))

# print(ql.mem.read(mem_value1, 0x10))
ql.mem.write(mem_value3, b"\x01")

def my_tolower(ql: Qiling,*args):
# return
ql.arch.regs.write("rax", ql.arch.regs.read("rdi"))


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

def my_strcmp(ql: Qiling):
ql.arch.regs.write("rax", 0)

def challenge10(ql: Qiling):
ql.os.set_api("strcmp", my_strcmp)


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

def challenge7(ql: Qiling):
# base = ql.mem.get_lib_base(ql.path)
# ql.hook_address(change_edi, base+0xF3c)
ql.os.set_syscall('nanosleep', my_nanosleep)
# ql.os.set_api('sleep', my_sleep)

def challenge6(ql: Qiling):
base = ql.mem.get_lib_base(ql.path)
ql.hook_address(hook_cmp_rax, base + 0xf16)
ql.hook_address(win, base + 0xEDD)

def change_regs(qi: Qiling):
ql.arch.regs.write("esi", 0x696C6951)
ql.arch.regs.write("ecx", 0x614C676E)
ql.arch.regs.write("eax", 0x20202062)

def challenge11(ql: Qiling):
base = ql.mem.get_lib_base(ql.path)
ql.hook_address(change_regs, base + 0x1195)

def change_eax_func(ql: Qiling):
ql.arch.regs.write("eax", 1)

def win(ql: Qiling):
print('[*] win')






def my_syscall_uname(ql: Qiling, *args) -> int:
"""
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

if __name__ == '__main__':
ql = Qiling(["qilinglab-x86_64"], r'./qiling/examples/rootfs/x8664_linux', verbose=QL_VERBOSE.OFF)

ql.verbose = 0
# ql.debugger = "gdb:0.0.0.0:12345"
challenge1(ql)
challenge2(ql)
challenge3(ql)
challenge4(ql)
challenge5(ql)
challenge6(ql)
challenge7(ql)
challenge8(ql)
challenge9(ql)
challenge10(ql)
challenge11(ql)
ql.run()


image-20240510230608132

注意&&报错

注意事项

这是做 lab 时的一些注意事项和报错解决 踩过的坑🥲

  1. ql.mem.write 向内存中写入数据时,变量类型必须是 bytes

  2. 在挑战4中使用了 ql.mem.get_lib_base(ql.path) 发生了报错,其原因是 ql.path 获取的路径为绝对路径,这样给 ql.mem.get_lib_base 函数会出问题。

    最终发现是在 main 函数中下面的代码,要写成 qilinglab-x86_64 这个路径才可以让 ql.path 获取相对路径

Qiling([r'qilinglab-x86_64'], r'/home/zikh/Desktop/qiling/examples/rootfs/x8664_linux', verbose=QL_VERBOSE.OFF)

从而使 ql.mem.get_lib_base 成功获得程序基地址

  1. qiling 版本为 1.4.6 (目前写下这篇文章的最新版是 1.4.6)中去调试 x86 x64 架构的程序时会遇到只能调试一次的情况,就是 si 或者 c 只能走一次。这应该是最新版的 bug 。用 pip uninstall qiling 卸载当前版本的 qiling ,再用命令 pip install qiling==1.4.5 安装一个老版本的 qiling 就可以正常调试 x86 的程序了

报错

from qiling import *
from qiling.const import QL_INTERCEPT
from qiling.const import QL_VERBOSE
from qiling.os.mapper import QlFsMappedObject
import os

if __name__ == '__main__':
ql = Qiling(["./test"], r'./qiling/examples/rootfs/mipsel-linux-gnu', verbose=QL_VERBOSE.OFF)

ql.verbose = 0
ql.debugger = "gdb:0.0.0.0:9999"

ql.run()

这样的脚本会有下面的报错

Traceback (most recent call last):
File "debug.py", line 15, in <module>
ql.run()
File "/home/zikh/.local/lib/python3.8/site-packages/qiling/core.py", line 599, in run
debugger.run()
File "/home/zikh/.local/lib/python3.8/site-packages/qiling/debugger/gdb/gdb.py", line 774, in run
reply = handler(subcmd.decode(ENCODING))
File "/home/zikh/.local/lib/python3.8/site-packages/qiling/debugger/gdb/gdb.py", line 503, in handle_q
return f'l{self.ql.os.path.host_to_virtual_path(self.ql.path)}'
File "/home/zikh/.local/lib/python3.8/site-packages/qiling/os/path.py", line 278, in host_to_virtual_path
virtpath = self._cwd_anchor / resolved.relative_to(self._rootfs_path)
File "/usr/lib/python3.8/pathlib.py", line 908, in relative_to
raise ValueError("{!r} does not start with {!r}"
ValueError: '/home/zikh/Desktop/test' does not start with '/home/zikh/Desktop/qiling/examples/rootfs/mipsel-linux-gnu'

解决方法,将 test 文件拷贝到 /home/zikh/Desktop/qiling/examples/rootfs/mipsel-linux-gnu/bin/test 路径

修改后的脚本应为

from qiling import *
from qiling.const import QL_INTERCEPT
from qiling.const import QL_VERBOSE
from qiling.os.mapper import QlFsMappedObject
import os

if __name__ == '__main__':
ql = Qiling(["/home/zikh/Desktop/qiling/examples/rootfs/mipsel-linux-gnu/bin/test"], r'./qiling/examples/rootfs/mipsel-linux-gnu', verbose=QL_VERBOSE.OFF)
ql.verbose = 0
ql.debugger = "gdb:0.0.0.0:9999"
ql.run()

参考文章

Qiling入门与QilingLab | Closure (zh-closure.github.io)

劫持 - 麒麟框架文档 — Hijack - Qiling Framework Documentation

[原创] Qiling框架分析实战:从 QilingLab 详解到 Qiling 源码分析-智能设备-看雪-安全社区|安全招聘|kanxue.com

初探 qiling ( 麒麟 ):开源的二进制分析、高级代码模拟框架_qiling安装-CSDN博客