前言 这篇文章也是好几个月之前写的了,当时因为忙了其他事情,一直没有整理发布,刚才无意中看到了以前的草稿,就索性发一下。Qiling 这个框架对于模拟运行二进制程序时的 hook 非常方便,可以很细致的获取或修改某个时刻的内存值,学习了该框架,可以后续在此基础上开发一些小工具。
其实网上关于 qiling-lab
资料已经非常多了,自学起来已经没有什么难度。本文也是照猫画虎完成了 qiling-lab 的 11 个挑战,并没有做其他的延伸学习,更多的记录是自己在学习过程时踩过的坑和一些思考。
qiling安装
一条命令直接安装,不过我在 ubuntu18.04
中遇到了一个报错
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
上正常安装
上面是用 -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
的配置
输出如下
[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; 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 ))
Pack and Unpack - Qiling Framework Documentation
challenge2 unsigned __int64 __fastcall challenge2 (_BYTE *a1) { unsigned int v2; int v3; int v4; int v5; struct utsname name ; char s[10 ]; char v8[16 ]; unsigned __int64 v9; v9 = __readfsqword(0x28 u); 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(0x28 u) ^ v9; }
让 uname
函数执行完,使其参数 sysname
和 version
解析成指定的字符串。这个需要用到 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; int i; int fd; char v5; char buf[32 ]; char v7[40 ]; unsigned __int64 v8; v8 = __readfsqword(0x28 u); fd = open("/dev/urandom" , 0 ); read(fd, buf, 0x20 uLL); 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(0x28 u) ^ v8; }
从 /dev/urandom
文件中读取 0x20
字节的随机数 ,再用 getrandom
函数生成 0x20
字节的随机数,两组随机数一样算作通关。
先使用 ql.os.set_syscall
对 getrandom
系统调用进行 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 ; }
看汇编发现是有个循环,从 0xE33
的 jmp
进入循环
.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; int i; int j; int v5[14 ]; unsigned __int64 v6; v6 = __readfsqword(0x28 u); 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(0x28 u) ^ v6; } } *a1 = 1 ; return __readfsqword(0x28 u) ^ 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
指令后(eax
为 1
)将 ZF
标志位寄存器清零。而 jnz
指令指的是 ZF
寄存器为 0
时跳转, [rbp+var_5]
的内容一直为 1
,从而陷入了一个死循环。
PS:这里记一下 jnz
指令,这个指令本身的全称是 jump if not zero
,翻译过来虽然是不为零时跳转,但这个不为零指的并不是 ZF
寄存器的值,而是运算结果( cmp
让两个操作数相减的结果 )。运算结果和 ZF
寄存器的值刚好是相反的关系,因此 ZF==0
时 jnz
才跳转
.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 ): ql.os.set_api('sleep' , my_sleep)
然后看了其他师傅的文章,这里也可以用 ql.os.set_syscall
劫持 sleep
库函数中的系统调用 nanosleep
challenge8 _DWORD *__fastcall challenge8 (__int64 a1) { _DWORD *result; _DWORD *v2; v2 = malloc (0x18 uLL); *(_QWORD *)v2 = malloc (0x1E uLL); 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; _DWORD *v3; v3 = malloc (0x18 uLL); *v3 = malloc (0x1E uLL); 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; char dest[32 ]; char src[40 ]; unsigned __int64 v5; v5 = __readfsqword(0x28 u); strcpy (src, "aBcdeFghiJKlMnopqRstuVWxYz" ); src[27 ] = 0 ; strcpy (dest, src); for ( i = dest; *i; ++i ) *i = tolower (*i); *a1 = strcmp (src, dest) == 0 ; return __readfsqword(0x28 u) ^ v5; }
这一关会将 aBcdeFghiJKlMnopqRstuVWxYz
变成全小写将其比较。我最开始的思路是劫持 tolower
函数让 rdi
寄存器的值给 rax
就结束,就像下面这样
def my_tolower (ql: Qiling,*args ): ql.arch.regs.write("rax" , ql.arch.regs.read("rdi" ))
这样写这关也能过,之后看了其他师傅的 wp
才注意到 src
和 dest
开始全被写成字符串 aBcdeFghiJKlMnopqRstuVWxYz
了。因此可以 hook tolower
函数,让其直接 return
。后续 strcmp
函数做比较时自然就通过了。
challenge10 unsigned __int64 __fastcall challenge10 (_BYTE *a1) { int i; int fd; ssize_t v4; char buf[72 ]; unsigned __int64 v6; v6 = __readfsqword(0x28 u); fd = open("/proc/self/cmdline" , 0 ); if ( fd != -1 ) { v4 = read(fd, buf, 0x3F uLL); 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(0x28 u) ^ 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_INTERCEPTfrom qiling.const import QL_VERBOSEfrom qiling.os.mapper import QlFsMappedObjectimport osimport structimport timedef challenge1 (ql: Qiling ): ql.mem.map (0x1000 , 0x1000 ) 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 )) 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 )) ql.mem.write(mem_value3, b"\x01" ) def my_tolower (ql: Qiling,*args ): 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 ): ql.os.set_syscall('nanosleep' , my_nanosleep) 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 challenge1(ql) challenge2(ql) challenge3(ql) challenge4(ql) challenge5(ql) challenge6(ql) challenge7(ql) challenge8(ql) challenge9(ql) challenge10(ql) challenge11(ql) ql.run()
注意&&报错 注意事项 这是做 lab
时的一些注意事项和报错解决 踩过的坑🥲
ql.mem.write
向内存中写入数据时,变量类型必须是 bytes
在挑战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
成功获得程序基地址
在 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_INTERCEPTfrom qiling.const import QL_VERBOSEfrom qiling.os.mapper import QlFsMappedObjectimport osif __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_INTERCEPTfrom qiling.const import QL_VERBOSEfrom qiling.os.mapper import QlFsMappedObjectimport osif __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博客